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 "filesystem.h"
45 #include "interop_subtitle_asset.h"
46 #include "mono_picture_asset.h"
47 #include "mono_picture_frame.h"
48 #include "raw_convert.h"
50 #include "reel_closed_caption_asset.h"
51 #include "reel_interop_subtitle_asset.h"
52 #include "reel_markers_asset.h"
53 #include "reel_picture_asset.h"
54 #include "reel_sound_asset.h"
55 #include "reel_smpte_subtitle_asset.h"
56 #include "reel_subtitle_asset.h"
57 #include "smpte_subtitle_asset.h"
58 #include "stereo_picture_asset.h"
59 #include "stereo_picture_frame.h"
61 #include "verify_j2k.h"
62 #include <libxml/parserInternals.h>
63 #include <xercesc/dom/DOMAttr.hpp>
64 #include <xercesc/dom/DOMDocument.hpp>
65 #include <xercesc/dom/DOMError.hpp>
66 #include <xercesc/dom/DOMErrorHandler.hpp>
67 #include <xercesc/dom/DOMException.hpp>
68 #include <xercesc/dom/DOMImplementation.hpp>
69 #include <xercesc/dom/DOMImplementationLS.hpp>
70 #include <xercesc/dom/DOMImplementationRegistry.hpp>
71 #include <xercesc/dom/DOMLSParser.hpp>
72 #include <xercesc/dom/DOMLocator.hpp>
73 #include <xercesc/dom/DOMNamedNodeMap.hpp>
74 #include <xercesc/dom/DOMNodeList.hpp>
75 #include <xercesc/framework/LocalFileInputSource.hpp>
76 #include <xercesc/framework/MemBufInputSource.hpp>
77 #include <xercesc/parsers/AbstractDOMParser.hpp>
78 #include <xercesc/parsers/XercesDOMParser.hpp>
79 #include <xercesc/sax/HandlerBase.hpp>
80 #include <xercesc/util/PlatformUtils.hpp>
81 #include <boost/algorithm/string.hpp>
90 using std::dynamic_pointer_cast;
92 using std::make_shared;
96 using std::shared_ptr;
99 using boost::optional;
100 using boost::function;
104 using namespace xercesc;
109 xml_ch_to_string (XMLCh const * a)
111 char* x = XMLString::transcode(a);
113 XMLString::release(&x);
118 class XMLValidationError
121 XMLValidationError (SAXParseException const & e)
122 : _message (xml_ch_to_string(e.getMessage()))
123 , _line (e.getLineNumber())
124 , _column (e.getColumnNumber())
125 , _public_id (e.getPublicId() ? xml_ch_to_string(e.getPublicId()) : "")
126 , _system_id (e.getSystemId() ? xml_ch_to_string(e.getSystemId()) : "")
131 string message () const {
135 uint64_t line () const {
139 uint64_t column () const {
143 string public_id () const {
147 string system_id () const {
160 class DCPErrorHandler : public ErrorHandler
163 void warning(const SAXParseException& e) override
165 maybe_add (XMLValidationError(e));
168 void error(const SAXParseException& e) override
170 maybe_add (XMLValidationError(e));
173 void fatalError(const SAXParseException& e) override
175 maybe_add (XMLValidationError(e));
178 void resetErrors() override {
182 list<XMLValidationError> errors () const {
187 void maybe_add (XMLValidationError e)
189 /* XXX: nasty hack */
191 e.message().find("schema document") != string::npos &&
192 e.message().find("has different target namespace from the one specified in instance document") != string::npos
197 _errors.push_back (e);
200 list<XMLValidationError> _errors;
207 StringToXMLCh (string a)
209 _buffer = XMLString::transcode(a.c_str());
212 StringToXMLCh (StringToXMLCh const&) = delete;
213 StringToXMLCh& operator= (StringToXMLCh const&) = delete;
217 XMLString::release (&_buffer);
220 XMLCh const * get () const {
229 class LocalFileResolver : public EntityResolver
232 LocalFileResolver (boost::filesystem::path xsd_dtd_directory)
233 : _xsd_dtd_directory (xsd_dtd_directory)
235 /* XXX: I'm not clear on what things need to be in this list; some XSDs are apparently, magically
236 * found without being here.
238 add("http://www.w3.org/2001/XMLSchema.dtd", "XMLSchema.dtd");
239 add("http://www.w3.org/2001/03/xml.xsd", "xml.xsd");
240 add("http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd", "xmldsig-core-schema.xsd");
241 add("http://www.digicine.com/schemas/437-Y/2007/Main-Stereo-Picture-CPL.xsd", "Main-Stereo-Picture-CPL.xsd");
242 add("http://www.digicine.com/PROTO-ASDCP-CPL-20040511.xsd", "PROTO-ASDCP-CPL-20040511.xsd");
243 add("http://www.digicine.com/PROTO-ASDCP-PKL-20040311.xsd", "PROTO-ASDCP-PKL-20040311.xsd");
244 add("http://www.digicine.com/PROTO-ASDCP-AM-20040311.xsd", "PROTO-ASDCP-AM-20040311.xsd");
245 add("http://www.digicine.com/PROTO-ASDCP-CC-CPL-20070926#", "PROTO-ASDCP-CC-CPL-20070926.xsd");
246 add("interop-subs", "DCSubtitle.v1.mattsson.xsd");
247 add("http://www.smpte-ra.org/schemas/428-7/2010/DCST.xsd", "DCDMSubtitle-2010.xsd");
248 add("http://www.smpte-ra.org/schemas/428-7/2014/DCST.xsd", "DCDMSubtitle-2014.xsd");
249 add("http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata", "SMPTE-429-16.xsd");
250 add("http://www.dolby.com/schemas/2012/AD", "Dolby-2012-AD.xsd");
251 add("http://www.smpte-ra.org/schemas/429-10/2008/Main-Stereo-Picture-CPL", "SMPTE-429-10-2008.xsd");
254 InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id) override
259 auto system_id_str = xml_ch_to_string (system_id);
260 auto p = _xsd_dtd_directory;
261 if (_files.find(system_id_str) == _files.end()) {
264 p /= _files[system_id_str];
266 StringToXMLCh ch (p.string());
267 return new LocalFileInputSource(ch.get());
271 void add (string uri, string file)
276 std::map<string, string> _files;
277 boost::filesystem::path _xsd_dtd_directory;
282 parse (XercesDOMParser& parser, boost::filesystem::path xml)
284 parser.parse(xml.c_str());
289 parse (XercesDOMParser& parser, string xml)
291 xercesc::MemBufInputSource buf(reinterpret_cast<unsigned char const*>(xml.c_str()), xml.size(), "");
298 validate_xml (T xml, boost::filesystem::path xsd_dtd_directory, vector<VerificationNote>& notes)
301 XMLPlatformUtils::Initialize ();
302 } catch (XMLException& e) {
303 throw MiscError ("Failed to initialise xerces library");
306 DCPErrorHandler error_handler;
308 /* All the xerces objects in this scope must be destroyed before XMLPlatformUtils::Terminate() is called */
310 XercesDOMParser parser;
311 parser.setValidationScheme(XercesDOMParser::Val_Always);
312 parser.setDoNamespaces(true);
313 parser.setDoSchema(true);
315 vector<string> schema;
316 schema.push_back("xml.xsd");
317 schema.push_back("xmldsig-core-schema.xsd");
318 schema.push_back("SMPTE-429-7-2006-CPL.xsd");
319 schema.push_back("SMPTE-429-8-2006-PKL.xsd");
320 schema.push_back("SMPTE-429-9-2007-AM.xsd");
321 schema.push_back("Main-Stereo-Picture-CPL.xsd");
322 schema.push_back("PROTO-ASDCP-CPL-20040511.xsd");
323 schema.push_back("PROTO-ASDCP-PKL-20040311.xsd");
324 schema.push_back("PROTO-ASDCP-AM-20040311.xsd");
325 schema.push_back("DCSubtitle.v1.mattsson.xsd");
326 schema.push_back("DCDMSubtitle-2010.xsd");
327 schema.push_back("DCDMSubtitle-2014.xsd");
328 schema.push_back("PROTO-ASDCP-CC-CPL-20070926.xsd");
329 schema.push_back("SMPTE-429-16.xsd");
330 schema.push_back("Dolby-2012-AD.xsd");
331 schema.push_back("SMPTE-429-10-2008.xsd");
332 schema.push_back("xlink.xsd");
333 schema.push_back("SMPTE-335-2012.xsd");
334 schema.push_back("SMPTE-395-2014-13-1-aaf.xsd");
335 schema.push_back("isdcf-mca.xsd");
336 schema.push_back("SMPTE-429-12-2008.xsd");
338 /* XXX: I'm not especially clear what this is for, but it seems to be necessary.
339 * Schemas that are not mentioned in this list are not read, and the things
340 * they describe are not checked.
343 for (auto i: schema) {
344 locations += String::compose("%1 %1 ", i, i);
347 parser.setExternalSchemaLocation(locations.c_str());
348 parser.setValidationSchemaFullChecking(true);
349 parser.setErrorHandler(&error_handler);
351 LocalFileResolver resolver (xsd_dtd_directory);
352 parser.setEntityResolver(&resolver);
355 parser.resetDocumentPool();
357 } catch (XMLException& e) {
358 throw MiscError(xml_ch_to_string(e.getMessage()));
359 } catch (DOMException& e) {
360 throw MiscError(xml_ch_to_string(e.getMessage()));
362 throw MiscError("Unknown exception from xerces");
366 XMLPlatformUtils::Terminate ();
368 for (auto i: error_handler.errors()) {
370 VerificationNote::Type::ERROR,
371 VerificationNote::Code::INVALID_XML,
373 boost::trim_copy(i.public_id() + " " + i.system_id()),
380 enum class VerifyAssetResult {
387 static VerifyAssetResult
388 verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelFileAsset> reel_file_asset, function<void (float)> progress)
390 /* When reading the DCP the hash will have been set to the one from the PKL/CPL.
391 * We want to calculate the hash of the actual file contents here, so that we
392 * can check it. unset_hash() means that this calculation will happen on the
395 reel_file_asset->asset_ref()->unset_hash();
396 auto const actual_hash = reel_file_asset->asset_ref()->hash([progress](int64_t done, int64_t total) {
397 progress(float(done) / total);
400 auto pkls = dcp->pkls();
401 /* We've read this DCP in so it must have at least one PKL */
402 DCP_ASSERT (!pkls.empty());
404 auto asset = reel_file_asset->asset_ref().asset();
406 optional<string> pkl_hash;
408 pkl_hash = i->hash (reel_file_asset->asset_ref()->id());
414 DCP_ASSERT (pkl_hash);
416 auto cpl_hash = reel_file_asset->hash();
417 if (cpl_hash && *cpl_hash != *pkl_hash) {
418 return VerifyAssetResult::CPL_PKL_DIFFER;
421 if (actual_hash != *pkl_hash) {
422 return VerifyAssetResult::BAD;
425 return VerifyAssetResult::GOOD;
430 verify_language_tag (string tag, vector<VerificationNote>& notes)
433 LanguageTag test (tag);
434 } catch (LanguageTagError &) {
435 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, tag});
441 verify_picture_asset (shared_ptr<const ReelFileAsset> reel_file_asset, boost::filesystem::path file, vector<VerificationNote>& notes, function<void (float)> progress)
443 int biggest_frame = 0;
444 auto asset = dynamic_pointer_cast<PictureAsset>(reel_file_asset->asset_ref().asset());
445 auto const duration = asset->intrinsic_duration ();
447 auto check_and_add = [¬es](vector<VerificationNote> const& j2k_notes) {
448 for (auto i: j2k_notes) {
449 if (find(notes.begin(), notes.end(), i) == notes.end()) {
455 if (auto mono_asset = dynamic_pointer_cast<MonoPictureAsset>(reel_file_asset->asset_ref().asset())) {
456 auto reader = mono_asset->start_read ();
457 for (int64_t i = 0; i < duration; ++i) {
458 auto frame = reader->get_frame (i);
459 biggest_frame = max(biggest_frame, frame->size());
460 if (!mono_asset->encrypted() || mono_asset->key()) {
461 vector<VerificationNote> j2k_notes;
462 verify_j2k(frame, i, mono_asset->frame_rate().numerator, j2k_notes);
463 check_and_add (j2k_notes);
465 progress (float(i) / duration);
467 } else if (auto stereo_asset = dynamic_pointer_cast<StereoPictureAsset>(asset)) {
468 auto reader = stereo_asset->start_read ();
469 for (int64_t i = 0; i < duration; ++i) {
470 auto frame = reader->get_frame (i);
471 biggest_frame = max(biggest_frame, max(frame->left()->size(), frame->right()->size()));
472 if (!stereo_asset->encrypted() || stereo_asset->key()) {
473 vector<VerificationNote> j2k_notes;
474 verify_j2k(frame->left(), i, stereo_asset->frame_rate().numerator, j2k_notes);
475 verify_j2k(frame->right(), i, stereo_asset->frame_rate().numerator, j2k_notes);
476 check_and_add (j2k_notes);
478 progress (float(i) / duration);
483 static const int max_frame = rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
484 static const int risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
485 if (biggest_frame > max_frame) {
487 VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
489 } else if (biggest_frame > risky_frame) {
491 VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
498 verify_main_picture_asset (
499 shared_ptr<const DCP> dcp,
500 shared_ptr<const ReelPictureAsset> reel_asset,
501 function<void (string, optional<boost::filesystem::path>)> stage,
502 function<void (float)> progress,
503 VerificationOptions options,
504 vector<VerificationNote>& notes
507 auto asset = reel_asset->asset();
508 auto const file = *asset->file();
510 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
511 stage ("Checking picture asset hash", file);
512 auto const r = verify_asset (dcp, reel_asset, progress);
514 case VerifyAssetResult::BAD:
516 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file
519 case VerifyAssetResult::CPL_PKL_DIFFER:
521 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file
529 stage ("Checking picture frame sizes", asset->file());
530 verify_picture_asset (reel_asset, file, notes, progress);
532 /* Only flat/scope allowed by Bv2.1 */
534 asset->size() != Size(2048, 858) &&
535 asset->size() != Size(1998, 1080) &&
536 asset->size() != Size(4096, 1716) &&
537 asset->size() != Size(3996, 2160)) {
539 VerificationNote::Type::BV21_ERROR,
540 VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS,
541 String::compose("%1x%2", asset->size().width, asset->size().height),
546 /* Only 24, 25, 48fps allowed for 2K */
548 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
549 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
552 VerificationNote::Type::BV21_ERROR,
553 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
554 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
559 if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
560 /* Only 24fps allowed for 4K */
561 if (asset->edit_rate() != Fraction(24, 1)) {
563 VerificationNote::Type::BV21_ERROR,
564 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
565 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
570 /* Only 2D allowed for 4K */
571 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
573 VerificationNote::Type::BV21_ERROR,
574 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
575 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
587 boost::optional<string> subtitle_language;
588 boost::optional<int> audio_channels;
593 verify_main_sound_asset (
594 shared_ptr<const DCP> dcp,
595 shared_ptr<const ReelSoundAsset> reel_asset,
596 function<void (string, optional<boost::filesystem::path>)> stage,
597 function<void (float)> progress,
598 VerificationOptions options,
599 vector<VerificationNote>& notes,
603 auto asset = reel_asset->asset();
604 auto const file = *asset->file();
606 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
607 stage("Checking sound asset hash", file);
608 auto const r = verify_asset (dcp, reel_asset, progress);
610 case VerifyAssetResult::BAD:
611 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, file});
613 case VerifyAssetResult::CPL_PKL_DIFFER:
614 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, file});
621 if (!state.audio_channels) {
622 state.audio_channels = asset->channels();
623 } else if (*state.audio_channels != asset->channels()) {
624 notes.push_back({ VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS, file });
627 stage ("Checking sound asset metadata", file);
629 if (auto lang = asset->language()) {
630 verify_language_tag (*lang, notes);
632 if (asset->sampling_rate() != 48000) {
633 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), file});
639 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
641 /* XXX: is Language compulsory? */
642 if (reel_asset->language()) {
643 verify_language_tag (*reel_asset->language(), notes);
646 if (!reel_asset->entry_point()) {
647 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id() });
648 } else if (reel_asset->entry_point().get()) {
649 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
655 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
657 /* XXX: is Language compulsory? */
658 if (reel_asset->language()) {
659 verify_language_tag (*reel_asset->language(), notes);
662 if (!reel_asset->entry_point()) {
663 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
664 } else if (reel_asset->entry_point().get()) {
665 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
670 /** Verify stuff that is common to both subtitles and closed captions */
672 verify_smpte_timed_text_asset (
673 shared_ptr<const SMPTESubtitleAsset> asset,
674 optional<int64_t> reel_asset_duration,
675 vector<VerificationNote>& notes
678 if (asset->language()) {
679 verify_language_tag (*asset->language(), notes);
681 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
684 auto const size = filesystem::file_size(asset->file().get());
685 if (size > 115 * 1024 * 1024) {
687 { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file() }
691 /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
692 * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
694 auto fonts = asset->font_data ();
696 for (auto i: fonts) {
697 total_size += i.second.size();
699 if (total_size > 10 * 1024 * 1024) {
700 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
703 if (!asset->start_time()) {
704 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get() });
705 } else if (asset->start_time() != Time()) {
706 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() });
709 if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
712 VerificationNote::Type::BV21_ERROR,
713 VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
714 String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
721 /** Verify Interop subtitle / CCAP stuff */
723 verify_interop_text_asset(shared_ptr<const InteropSubtitleAsset> asset, vector<VerificationNote>& notes)
725 if (asset->subtitles().empty()) {
726 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_SUBTITLE, asset->id(), asset->file().get() });
728 auto const unresolved = asset->unresolved_fonts();
729 if (!unresolved.empty()) {
730 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_FONT, unresolved.front() });
735 /** Verify SMPTE subtitle-only stuff */
737 verify_smpte_subtitle_asset (
738 shared_ptr<const SMPTESubtitleAsset> asset,
739 vector<VerificationNote>& notes,
743 if (asset->language()) {
744 if (!state.subtitle_language) {
745 state.subtitle_language = *asset->language();
746 } else if (state.subtitle_language != *asset->language()) {
747 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
751 DCP_ASSERT (asset->resource_id());
752 auto xml_id = asset->xml_id();
754 if (asset->resource_id().get() != xml_id) {
755 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
758 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
759 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
762 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
765 if (asset->raw_xml()) {
766 /* Deluxe require this in their QC even if it seems never to be mentioned in any standard */
767 cxml::Document doc("SubtitleReel");
768 doc.read_string(*asset->raw_xml());
769 auto issue_date = doc.string_child("IssueDate");
770 std::regex reg("^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d$");
771 if (!std::regex_match(issue_date, reg)) {
772 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, issue_date});
778 /** Verify all subtitle stuff */
780 verify_subtitle_asset (
781 shared_ptr<const SubtitleAsset> asset,
782 optional<int64_t> reel_asset_duration,
783 function<void (string, optional<boost::filesystem::path>)> stage,
784 boost::filesystem::path xsd_dtd_directory,
785 vector<VerificationNote>& notes,
789 stage ("Checking subtitle XML", asset->file());
790 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
791 * gets passed through libdcp which may clean up and therefore hide errors.
793 if (asset->raw_xml()) {
794 validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
796 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
799 auto namespace_count = [](shared_ptr<const SubtitleAsset> asset, string root_node) {
800 cxml::Document doc(root_node);
801 doc.read_string(asset->raw_xml().get());
802 auto root = dynamic_cast<xmlpp::Element*>(doc.node())->cobj();
804 for (auto ns = root->nsDef; ns != nullptr; ns = ns->next) {
810 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
812 verify_interop_text_asset(interop, notes);
813 if (namespace_count(asset, "DCSubtitle") > 1) {
814 notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id() });
818 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
820 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
821 verify_smpte_subtitle_asset (smpte, notes, state);
822 /* This asset may be encrypted and in that case we'll have no raw_xml() */
823 if (asset->raw_xml() && namespace_count(asset, "SubtitleReel") > 1) {
824 notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id()});
830 /** Verify all closed caption stuff */
832 verify_closed_caption_asset (
833 shared_ptr<const SubtitleAsset> asset,
834 optional<int64_t> reel_asset_duration,
835 function<void (string, optional<boost::filesystem::path>)> stage,
836 boost::filesystem::path xsd_dtd_directory,
837 vector<VerificationNote>& notes
840 stage ("Checking closed caption XML", asset->file());
841 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
842 * gets passed through libdcp which may clean up and therefore hide errors.
844 auto raw_xml = asset->raw_xml();
846 validate_xml (*raw_xml, xsd_dtd_directory, notes);
847 if (raw_xml->size() > 256 * 1024) {
848 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
851 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
854 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
856 verify_interop_text_asset(interop, notes);
859 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
861 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
866 /** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes etc. */
869 verify_text_details (
870 dcp::Standard standard,
871 vector<shared_ptr<Reel>> reels,
873 vector<VerificationNote>& notes,
874 std::function<bool (shared_ptr<Reel>)> check,
875 std::function<optional<string> (shared_ptr<Reel>)> xml,
876 std::function<int64_t (shared_ptr<Reel>)> duration,
877 std::function<std::string (shared_ptr<Reel>)> id
880 /* end of last subtitle (in editable units) */
881 optional<int64_t> last_out;
882 auto too_short = false;
883 auto too_close = false;
884 auto too_early = false;
885 auto reel_overlap = false;
886 auto empty_text = false;
887 /* current reel start time (in editable units) */
888 int64_t reel_offset = 0;
889 optional<string> missing_load_font_id;
891 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool, bool&, vector<string>&)> parse;
893 parse = [&parse, &last_out, &too_short, &too_close, &too_early, &empty_text, &reel_offset, &missing_load_font_id](
894 cxml::ConstNodePtr node,
896 optional<Time> start_time,
900 vector<string>& font_ids
902 if (node->name() == "Subtitle") {
903 Time in (node->string_attribute("TimeIn"), tcr);
907 Time out (node->string_attribute("TimeOut"), tcr);
911 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
914 auto length = out - in;
915 if (length.as_editable_units_ceil(er) < 15) {
919 /* XXX: this feels dubious - is it really what Bv2.1 means? */
920 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
921 if (distance >= 0 && distance < 2) {
925 last_out = reel_offset + out.as_editable_units_floor(er);
926 } else if (node->name() == "Text") {
927 std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
928 if (!node->content().empty()) {
931 for (auto i: node->node_children()) {
932 if (node_has_content(i)) {
938 if (!node_has_content(node)) {
942 } else if (node->name() == "LoadFont") {
943 if (auto const id = node->optional_string_attribute("Id")) {
944 font_ids.push_back(*id);
945 } else if (auto const id = node->optional_string_attribute("ID")) {
946 font_ids.push_back(*id);
948 } else if (node->name() == "Font") {
949 if (auto const font_id = node->optional_string_attribute("Id")) {
950 if (std::find_if(font_ids.begin(), font_ids.end(), [font_id](string const& id) { return id == font_id; }) == font_ids.end()) {
951 missing_load_font_id = font_id;
955 for (auto i: node->node_children()) {
956 parse(i, tcr, start_time, er, first_reel, has_text, font_ids);
960 for (auto i = 0U; i < reels.size(); ++i) {
961 if (!check(reels[i])) {
965 auto reel_xml = xml(reels[i]);
967 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
971 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
972 * read in by libdcp's parser.
975 shared_ptr<cxml::Document> doc;
977 optional<Time> start_time;
979 case dcp::Standard::INTEROP:
980 doc = make_shared<cxml::Document>("DCSubtitle");
981 doc->read_string (*reel_xml);
983 case dcp::Standard::SMPTE:
984 doc = make_shared<cxml::Document>("SubtitleReel");
985 doc->read_string (*reel_xml);
986 tcr = doc->number_child<int>("TimeCodeRate");
987 if (auto start_time_string = doc->optional_string_child("StartTime")) {
988 start_time = Time(*start_time_string, tcr);
992 bool has_text = false;
993 vector<string> font_ids;
994 parse(doc, tcr, start_time, edit_rate, i == 0, has_text, font_ids);
995 auto end = reel_offset + duration(reels[i]);
996 if (last_out && *last_out > end) {
1001 if (standard == dcp::Standard::SMPTE && has_text && font_ids.empty()) {
1002 notes.push_back(dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT).set_id(id(reels[i])));
1006 if (last_out && *last_out > reel_offset) {
1007 reel_overlap = true;
1012 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
1018 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
1024 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
1030 VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
1036 VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_TEXT
1040 if (missing_load_font_id) {
1041 notes.push_back(dcp::VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT).set_id(*missing_load_font_id));
1048 verify_closed_caption_details (
1049 vector<shared_ptr<Reel>> reels,
1050 vector<VerificationNote>& notes
1053 std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
1054 find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
1055 for (auto i: node->node_children()) {
1056 if (i->name() == "Text") {
1057 text_or_image.push_back (i);
1059 find_text_or_image (i, text_or_image);
1064 auto mismatched_valign = false;
1065 auto incorrect_order = false;
1067 std::function<void (cxml::ConstNodePtr)> parse;
1068 parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
1069 if (node->name() == "Subtitle") {
1070 vector<cxml::ConstNodePtr> text_or_image;
1071 find_text_or_image (node, text_or_image);
1072 optional<string> last_valign;
1073 optional<float> last_vpos;
1074 for (auto i: text_or_image) {
1075 auto valign = i->optional_string_attribute("VAlign");
1077 valign = i->optional_string_attribute("Valign").get_value_or("center");
1079 auto vpos = i->optional_number_attribute<float>("VPosition");
1081 vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
1085 if (*last_valign != valign) {
1086 mismatched_valign = true;
1089 last_valign = valign;
1091 if (!mismatched_valign) {
1093 if (*last_valign == "top" || *last_valign == "center") {
1094 if (*vpos < *last_vpos) {
1095 incorrect_order = true;
1098 if (*vpos > *last_vpos) {
1099 incorrect_order = true;
1108 for (auto i: node->node_children()) {
1113 for (auto reel: reels) {
1114 for (auto ccap: reel->closed_captions()) {
1115 auto reel_xml = ccap->asset()->raw_xml();
1117 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
1121 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1122 * read in by libdcp's parser.
1125 shared_ptr<cxml::Document> doc;
1127 optional<Time> start_time;
1129 doc = make_shared<cxml::Document>("SubtitleReel");
1130 doc->read_string (*reel_xml);
1132 doc = make_shared<cxml::Document>("DCSubtitle");
1133 doc->read_string (*reel_xml);
1139 if (mismatched_valign) {
1141 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN,
1145 if (incorrect_order) {
1147 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING,
1153 struct LinesCharactersResult
1155 bool warning_length_exceeded = false;
1156 bool error_length_exceeded = false;
1157 bool line_count_exceeded = false;
1163 verify_text_lines_and_characters (
1164 shared_ptr<SubtitleAsset> asset,
1167 LinesCharactersResult* result
1173 Event (Time time_, float position_, int characters_)
1175 , position (position_)
1176 , characters (characters_)
1179 Event (Time time_, shared_ptr<Event> start_)
1185 int position; //< position from 0 at top of screen to 100 at bottom
1187 shared_ptr<Event> start;
1190 vector<shared_ptr<Event>> events;
1192 auto position = [](shared_ptr<const SubtitleString> sub) {
1193 switch (sub->v_align()) {
1195 return lrintf(sub->v_position() * 100);
1196 case VAlign::CENTER:
1197 return lrintf((0.5f + sub->v_position()) * 100);
1198 case VAlign::BOTTOM:
1199 return lrintf((1.0f - sub->v_position()) * 100);
1205 for (auto j: asset->subtitles()) {
1206 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1208 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1209 events.push_back(in);
1210 events.push_back(make_shared<Event>(text->out(), in));
1214 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1215 return a->time < b->time;
1218 map<int, int> current;
1219 for (auto i: events) {
1220 if (current.size() > 3) {
1221 result->line_count_exceeded = true;
1223 for (auto j: current) {
1224 if (j.second > warning_length) {
1225 result->warning_length_exceeded = true;
1227 if (j.second > error_length) {
1228 result->error_length_exceeded = true;
1233 /* end of a subtitle */
1234 DCP_ASSERT (current.find(i->start->position) != current.end());
1235 if (current[i->start->position] == i->start->characters) {
1236 current.erase(i->start->position);
1238 current[i->start->position] -= i->start->characters;
1241 /* start of a subtitle */
1242 if (current.find(i->position) == current.end()) {
1243 current[i->position] = i->characters;
1245 current[i->position] += i->characters;
1254 verify_text_details(dcp::Standard standard, vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1256 if (reels.empty()) {
1260 if (reels[0]->main_subtitle()) {
1261 verify_text_details(standard, reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1262 [](shared_ptr<Reel> reel) {
1263 return static_cast<bool>(reel->main_subtitle());
1265 [](shared_ptr<Reel> reel) {
1266 return reel->main_subtitle()->asset()->raw_xml();
1268 [](shared_ptr<Reel> reel) {
1269 return reel->main_subtitle()->actual_duration();
1271 [](shared_ptr<Reel> reel) {
1272 return reel->main_subtitle()->id();
1277 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1278 verify_text_details(standard, reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1279 [i](shared_ptr<Reel> reel) {
1280 return i < reel->closed_captions().size();
1282 [i](shared_ptr<Reel> reel) {
1283 return reel->closed_captions()[i]->asset()->raw_xml();
1285 [i](shared_ptr<Reel> reel) {
1286 return reel->closed_captions()[i]->actual_duration();
1288 [i](shared_ptr<Reel> reel) {
1289 return reel->closed_captions()[i]->id();
1294 verify_closed_caption_details (reels, notes);
1299 verify_extension_metadata(shared_ptr<const CPL> cpl, vector<VerificationNote>& notes)
1301 DCP_ASSERT (cpl->file());
1302 cxml::Document doc ("CompositionPlaylist");
1303 doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1305 auto missing = false;
1308 if (auto reel_list = doc.node_child("ReelList")) {
1309 auto reels = reel_list->node_children("Reel");
1310 if (!reels.empty()) {
1311 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1312 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1313 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1315 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1316 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1320 if (auto name = extension->optional_node_child("Name")) {
1321 if (name->content() != "Application") {
1322 malformed = "<Name> should be 'Application'";
1325 if (auto property_list = extension->optional_node_child("PropertyList")) {
1326 if (auto property = property_list->optional_node_child("Property")) {
1327 if (auto name = property->optional_node_child("Name")) {
1328 if (name->content() != "DCP Constraints Profile") {
1329 malformed = "<Name> property should be 'DCP Constraints Profile'";
1332 if (auto value = property->optional_node_child("Value")) {
1333 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1334 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1349 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1350 } else if (!malformed.empty()) {
1351 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1357 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1359 vector<string> encrypted;
1360 for (auto i: dcp->cpls()) {
1361 for (auto j: i->reel_file_assets()) {
1362 if (j->asset_ref().resolved()) {
1363 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1364 if (mxf && mxf->encrypted()) {
1365 encrypted.push_back(j->asset_ref().id());
1371 for (auto i: pkl->assets()) {
1372 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1384 shared_ptr<const DCP> dcp,
1385 shared_ptr<const CPL> cpl,
1386 shared_ptr<const Reel> reel,
1387 optional<dcp::Size> main_picture_active_area,
1388 function<void (string, optional<boost::filesystem::path>)> stage,
1389 boost::filesystem::path xsd_dtd_directory,
1390 function<void (float)> progress,
1391 VerificationOptions options,
1392 vector<VerificationNote>& notes,
1394 bool* have_main_subtitle,
1395 bool* have_no_main_subtitle,
1396 size_t* most_closed_captions,
1397 size_t* fewest_closed_captions,
1398 map<Marker, Time>* markers_seen
1401 for (auto i: reel->assets()) {
1402 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1403 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1405 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1406 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1408 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1409 if (i->encryptable() && !file_asset->hash()) {
1410 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1414 if (dcp->standard() == Standard::SMPTE) {
1415 boost::optional<int64_t> duration;
1416 for (auto i: reel->assets()) {
1418 duration = i->actual_duration();
1419 } else if (*duration != i->actual_duration()) {
1420 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1426 if (reel->main_picture()) {
1427 /* Check reel stuff */
1428 auto const frame_rate = reel->main_picture()->frame_rate();
1429 if (frame_rate.denominator != 1 ||
1430 (frame_rate.numerator != 24 &&
1431 frame_rate.numerator != 25 &&
1432 frame_rate.numerator != 30 &&
1433 frame_rate.numerator != 48 &&
1434 frame_rate.numerator != 50 &&
1435 frame_rate.numerator != 60 &&
1436 frame_rate.numerator != 96)) {
1438 VerificationNote::Type::ERROR,
1439 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1440 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1444 if (reel->main_picture()->asset_ref().resolved()) {
1445 verify_main_picture_asset(dcp, reel->main_picture(), stage, progress, options, notes);
1446 auto const asset_size = reel->main_picture()->asset()->size();
1447 if (main_picture_active_area) {
1448 if (main_picture_active_area->width > asset_size.width) {
1450 VerificationNote::Type::ERROR,
1451 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1452 String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1456 if (main_picture_active_area->height > asset_size.height) {
1458 VerificationNote::Type::ERROR,
1459 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1460 String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1469 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1470 verify_main_sound_asset(dcp, reel->main_sound(), stage, progress, options, notes, state);
1473 if (reel->main_subtitle()) {
1474 verify_main_subtitle_reel(reel->main_subtitle(), notes);
1475 if (reel->main_subtitle()->asset_ref().resolved()) {
1476 verify_subtitle_asset(reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, xsd_dtd_directory, notes, state);
1478 *have_main_subtitle = true;
1480 *have_no_main_subtitle = true;
1483 for (auto i: reel->closed_captions()) {
1484 verify_closed_caption_reel(i, notes);
1485 if (i->asset_ref().resolved()) {
1486 verify_closed_caption_asset(i->asset(), i->duration(), stage, xsd_dtd_directory, notes);
1490 if (reel->main_markers()) {
1491 for (auto const& i: reel->main_markers()->get()) {
1492 markers_seen->insert(i);
1494 if (reel->main_markers()->entry_point()) {
1495 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_ENTRY_POINT});
1497 if (reel->main_markers()->duration()) {
1498 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_DURATION});
1502 *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1503 *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1511 shared_ptr<const DCP> dcp,
1512 shared_ptr<const CPL> cpl,
1513 function<void (string, optional<boost::filesystem::path>)> stage,
1514 boost::filesystem::path xsd_dtd_directory,
1515 function<void (float)> progress,
1516 VerificationOptions options,
1517 vector<VerificationNote>& notes,
1521 stage("Checking CPL", cpl->file());
1522 validate_xml(cpl->file().get(), xsd_dtd_directory, notes);
1524 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1525 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1528 for (auto const& i: cpl->additional_subtitle_languages()) {
1529 verify_language_tag(i, notes);
1532 if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1533 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1534 * of the approved ones.
1536 auto all = ContentKind::all();
1537 auto name = cpl->content_kind().name();
1538 transform(name.begin(), name.end(), name.begin(), ::tolower);
1539 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1540 if (iter == all.end()) {
1541 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name()});
1545 if (cpl->release_territory()) {
1546 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") {
1547 auto terr = cpl->release_territory().get();
1548 /* Must be a valid region tag, or "001" */
1550 LanguageTag::RegionSubtag test(terr);
1552 if (terr != "001") {
1553 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1559 for (auto version: cpl->content_versions()) {
1560 if (version.label_text.empty()) {
1562 dcp::VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT, cpl->file().get()).set_id(cpl->id())
1568 if (dcp->standard() == Standard::SMPTE) {
1569 if (!cpl->annotation_text()) {
1570 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1571 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1572 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1576 for (auto i: dcp->pkls()) {
1577 /* Check that the CPL's hash corresponds to the PKL */
1578 optional<string> h = i->hash(cpl->id());
1579 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1580 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1583 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1584 optional<string> required_annotation_text;
1585 for (auto j: i->assets()) {
1586 /* See if this is a CPL */
1587 for (auto k: dcp->cpls()) {
1588 if (j->id() == k->id()) {
1589 if (!required_annotation_text) {
1590 /* First CPL we have found; this is the required AnnotationText unless we find another */
1591 required_annotation_text = cpl->content_title_text();
1593 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1594 required_annotation_text = boost::none;
1600 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1601 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1605 /* set to true if any reel has a MainSubtitle */
1606 auto have_main_subtitle = false;
1607 /* set to true if any reel has no MainSubtitle */
1608 auto have_no_main_subtitle = false;
1609 /* fewest number of closed caption assets seen in a reel */
1610 size_t fewest_closed_captions = SIZE_MAX;
1611 /* most number of closed caption assets seen in a reel */
1612 size_t most_closed_captions = 0;
1613 map<Marker, Time> markers_seen;
1615 auto const main_picture_active_area = cpl->main_picture_active_area();
1616 if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1618 VerificationNote::Type::ERROR,
1619 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1620 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1624 if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1626 VerificationNote::Type::ERROR,
1627 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1628 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1633 for (auto reel: cpl->reels()) {
1634 stage("Checking reel", optional<boost::filesystem::path>());
1639 main_picture_active_area,
1646 &have_main_subtitle,
1647 &have_no_main_subtitle,
1648 &most_closed_captions,
1649 &fewest_closed_captions,
1654 verify_text_details(dcp->standard().get_value_or(dcp::Standard::SMPTE), cpl->reels(), notes);
1656 if (dcp->standard() == Standard::SMPTE) {
1657 if (auto msc = cpl->main_sound_configuration()) {
1658 if (state.audio_channels && msc->channels() != *state.audio_channels) {
1660 VerificationNote::Type::ERROR,
1661 VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION,
1662 String::compose("MainSoundConfiguration has %1 channels but sound assets have %2", msc->channels(), *state.audio_channels),
1668 if (have_main_subtitle && have_no_main_subtitle) {
1669 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1672 if (fewest_closed_captions != most_closed_captions) {
1673 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1676 if (cpl->content_kind() == ContentKind::FEATURE) {
1677 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1678 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1680 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1681 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1685 auto ffoc = markers_seen.find(Marker::FFOC);
1686 if (ffoc == markers_seen.end()) {
1687 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1688 } else if (ffoc->second.e != 1) {
1689 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1692 auto lfoc = markers_seen.find(Marker::LFOC);
1693 if (lfoc == markers_seen.end()) {
1694 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1696 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1697 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1698 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1702 LinesCharactersResult result;
1703 for (auto reel: cpl->reels()) {
1704 if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1705 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1709 if (result.line_count_exceeded) {
1710 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1712 if (result.error_length_exceeded) {
1713 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1714 } else if (result.warning_length_exceeded) {
1715 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1718 result = LinesCharactersResult();
1719 for (auto reel: cpl->reels()) {
1720 for (auto i: reel->closed_captions()) {
1722 verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1727 if (result.line_count_exceeded) {
1728 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1730 if (result.error_length_exceeded) {
1731 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1734 if (!cpl->read_composition_metadata()) {
1735 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1736 } else if (!cpl->version_number()) {
1737 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1740 verify_extension_metadata(cpl, notes);
1742 if (cpl->any_encrypted()) {
1743 cxml::Document doc("CompositionPlaylist");
1744 DCP_ASSERT(cpl->file());
1745 doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1746 if (!doc.optional_node_child("Signature")) {
1747 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1757 shared_ptr<const DCP> dcp,
1758 shared_ptr<const PKL> pkl,
1759 boost::filesystem::path xsd_dtd_directory,
1760 vector<VerificationNote>& notes
1763 validate_xml(pkl->file().get(), xsd_dtd_directory, notes);
1765 if (pkl_has_encrypted_assets(dcp, pkl)) {
1766 cxml::Document doc("PackingList");
1767 doc.read_file(dcp::filesystem::fix_long_path(pkl->file().get()));
1768 if (!doc.optional_node_child("Signature")) {
1769 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1773 set<string> uuid_set;
1774 for (auto asset: pkl->assets()) {
1775 if (!uuid_set.insert(asset->id()).second) {
1776 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get()});
1787 shared_ptr<const DCP> dcp,
1788 boost::filesystem::path xsd_dtd_directory,
1789 vector<VerificationNote>& notes
1792 auto asset_map = dcp->asset_map();
1793 DCP_ASSERT(asset_map);
1795 validate_xml(asset_map->file().get(), xsd_dtd_directory, notes);
1797 set<string> uuid_set;
1798 for (auto const& asset: asset_map->assets()) {
1799 if (!uuid_set.insert(asset.id()).second) {
1800 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get()});
1807 vector<VerificationNote>
1809 vector<boost::filesystem::path> directories,
1810 vector<dcp::DecryptedKDM> kdms,
1811 function<void (string, optional<boost::filesystem::path>)> stage,
1812 function<void (float)> progress,
1813 VerificationOptions options,
1814 optional<boost::filesystem::path> xsd_dtd_directory
1817 if (!xsd_dtd_directory) {
1818 xsd_dtd_directory = resources_directory() / "xsd";
1820 *xsd_dtd_directory = filesystem::canonical(*xsd_dtd_directory);
1822 vector<VerificationNote> notes;
1825 vector<shared_ptr<DCP>> dcps;
1826 for (auto i: directories) {
1827 dcps.push_back (make_shared<DCP>(i));
1830 for (auto dcp: dcps) {
1831 stage ("Checking DCP", dcp->directory());
1832 bool carry_on = true;
1834 dcp->read (¬es, true);
1835 } catch (MissingAssetmapError& e) {
1836 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1838 } catch (ReadError& e) {
1839 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1840 } catch (XMLError& e) {
1841 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1842 } catch (MXFFileError& e) {
1843 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1844 } catch (BadURNUUIDError& e) {
1845 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1846 } catch (cxml::Error& e) {
1847 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1854 if (dcp->standard() != Standard::SMPTE) {
1855 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1858 for (auto kdm: kdms) {
1862 for (auto cpl: dcp->cpls()) {
1875 for (auto pkl: dcp->pkls()) {
1876 stage("Checking PKL", pkl->file());
1877 verify_pkl(dcp, pkl, *xsd_dtd_directory, notes);
1880 if (dcp->asset_map_file()) {
1881 stage("Checking ASSETMAP", dcp->asset_map_file().get());
1882 verify_assetmap(dcp, *xsd_dtd_directory, notes);
1884 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1893 dcp::note_to_string (VerificationNote note)
1895 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1897 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1898 * not "ClosedCaption assets must have an <EntryPoint> tag."
1900 * It's OK to use XML tag names where they are clear.
1901 * If both ID and filename are available, use only the ID.
1902 * End messages with a full stop.
1903 * Messages should not mention whether or not their errors are a part of Bv2.1.
1905 switch (note.code()) {
1906 case VerificationNote::Code::FAILED_READ:
1907 return *note.note();
1908 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1909 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1910 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1911 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1912 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1913 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1914 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1915 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1916 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1917 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1918 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1919 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1920 case VerificationNote::Code::EMPTY_ASSET_PATH:
1921 return "The asset map contains an empty asset path.";
1922 case VerificationNote::Code::MISSING_ASSET:
1923 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1924 case VerificationNote::Code::MISMATCHED_STANDARD:
1925 return "The DCP contains both SMPTE and Interop parts.";
1926 case VerificationNote::Code::INVALID_XML:
1927 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1928 case VerificationNote::Code::MISSING_ASSETMAP:
1929 return "No valid ASSETMAP or ASSETMAP.xml was found.";
1930 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1931 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1932 case VerificationNote::Code::INVALID_DURATION:
1933 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1934 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1935 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());
1936 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1937 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());
1938 case VerificationNote::Code::EXTERNAL_ASSET:
1939 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());
1940 case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1941 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1942 case VerificationNote::Code::INVALID_STANDARD:
1943 return "This DCP does not use the SMPTE standard.";
1944 case VerificationNote::Code::INVALID_LANGUAGE:
1945 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1946 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1947 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1948 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1949 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1950 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1951 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1952 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1953 return "3D 4K DCPs are not allowed.";
1954 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1955 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1956 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1957 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1958 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1959 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());
1960 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1961 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1962 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1963 return "Some subtitle assets have different <Language> tags than others";
1964 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1965 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1966 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1967 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1968 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1969 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1970 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1971 return "At least one subtitle lasts less than 15 frames.";
1972 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1973 return "At least one pair of subtitles is separated by less than 2 frames.";
1974 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1975 return "At least one subtitle extends outside of its reel.";
1976 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1977 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1978 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1979 return "There are more than 52 characters in at least one subtitle line.";
1980 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1981 return "There are more than 79 characters in at least one subtitle line.";
1982 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1983 return "There are more than 3 closed caption lines in at least one place.";
1984 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1985 return "There are more than 32 characters in at least one closed caption line.";
1986 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1987 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1988 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1989 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1990 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1991 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.note().get());
1992 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1993 return "All assets in a reel do not have the same duration.";
1994 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
1995 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
1996 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
1997 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1998 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
1999 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
2000 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
2001 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
2002 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
2003 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
2004 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
2005 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
2006 case VerificationNote::Code::MISSING_HASH:
2007 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
2008 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
2009 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
2010 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
2011 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
2012 case VerificationNote::Code::MISSING_FFOC:
2013 return "There should be a FFOC (first frame of content) marker.";
2014 case VerificationNote::Code::MISSING_LFOC:
2015 return "There should be a LFOC (last frame of content) marker.";
2016 case VerificationNote::Code::INCORRECT_FFOC:
2017 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
2018 case VerificationNote::Code::INCORRECT_LFOC:
2019 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
2020 case VerificationNote::Code::MISSING_CPL_METADATA:
2021 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
2022 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
2023 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
2024 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
2025 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
2026 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
2027 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
2028 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
2029 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
2030 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
2031 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
2032 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
2033 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
2034 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
2035 return "Some assets are encrypted but some are not.";
2036 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
2037 return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1).", note.note().get());
2038 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
2039 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
2040 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
2041 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
2042 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
2043 return "The JPEG2000 tile size is not the same as the image size.";
2044 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
2045 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
2046 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
2047 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
2048 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
2049 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
2050 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
2051 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
2052 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
2053 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
2054 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
2055 return "POC marker found outside main header.";
2056 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
2057 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
2058 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
2059 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
2060 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
2061 return "No TLM marker was found in a JPEG2000 codestream.";
2062 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
2063 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
2064 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
2065 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
2066 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
2068 vector<string> parts;
2069 boost::split (parts, note.note().get(), boost::is_any_of(" "));
2070 DCP_ASSERT (parts.size() == 2);
2071 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]);
2073 case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
2074 return "Some aspect of this DCP could not be checked because it is encrypted.";
2075 case VerificationNote::Code::EMPTY_TEXT:
2076 return "There is an empty <Text> node in a subtitle or closed caption.";
2077 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
2078 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
2079 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
2080 return "Some closed captions are not listed in the order of their vertical position.";
2081 case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
2082 return "There is an <EntryPoint> node inside a <MainMarkers>.";
2083 case VerificationNote::Code::UNEXPECTED_DURATION:
2084 return "There is an <Duration> node inside a <MainMarkers>.";
2085 case VerificationNote::Code::INVALID_CONTENT_KIND:
2086 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
2087 case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
2088 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
2089 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
2090 return String::compose("The PKL %1 has more than one asset with the same ID.", note.note().get());
2091 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
2092 return String::compose("The ASSETMAP %1 has more than one asset with the same ID.", note.note().get());
2093 case VerificationNote::Code::MISSING_SUBTITLE:
2094 return String::compose("The subtitle asset %1 has no subtitles.", note.note().get());
2095 case VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
2096 return String::compose("<IssueDate> has an invalid value: %1", note.note().get());
2097 case VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS:
2098 return String::compose("The sound assets do not all have the same channel count; the first to differ is %1", note.file()->filename());
2099 case VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION:
2100 return String::compose("<MainSoundConfiguration> has an invalid value: %1", note.note().get());
2101 case VerificationNote::Code::MISSING_FONT:
2102 return String::compose("The font file for font ID \"%1\" was not found, or was not referred to in the ASSETMAP.", note.note().get());
2103 case VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE:
2104 return String::compose(
2105 "Frame %1 has an image component that is too large (component %2 is %3 bytes in size).",
2106 note.frame().get(), note.component().get(), note.size().get()
2108 case VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT:
2109 return String::compose("The XML in the subtitle asset %1 has more than one namespace declaration.", note.note().get());
2110 case VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT:
2111 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());
2112 case VerificationNote::Code::MISSING_LOAD_FONT:
2113 return String::compose("The SMPTE subtitle asset %1 has <Text> nodes but no <LoadFont> node", note.id().get());
2114 case VerificationNote::Code::MISMATCHED_ASSET_MAP_ID:
2115 return String::compose("The asset with ID %1 in the asset map actually has an id of %2", note.id().get(), note.other_id().get());
2116 case VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT:
2117 return String::compose("The <LabelText> in a <ContentVersion> in CPL %1 is empty", note.id().get());
2125 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2127 return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
2132 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2134 if (a.type() != b.type()) {
2135 return a.type() < b.type();
2138 if (a.code() != b.code()) {
2139 return a.code() < b.code();
2142 if (a.note() != b.note()) {
2143 return a.note().get_value_or("") < b.note().get_value_or("");
2146 if (a.file() != b.file()) {
2147 return a.file().get_value_or("") < b.file().get_value_or("");
2150 return a.line().get_value_or(0) < b.line().get_value_or(0);
2155 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2157 s << note_to_string (note);
2159 s << " [" << note.note().get() << "]";
2162 s << " [" << note.file().get() << "]";
2165 s << " [" << note.line().get() << "]";