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, int64_t start_frame, 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, start_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(), start_frame, i, stereo_asset->frame_rate().numerator, j2k_notes);
475 verify_j2k(frame->right(), start_frame, 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,
502 function<void (string, optional<boost::filesystem::path>)> stage,
503 function<void (float)> progress,
504 VerificationOptions options,
505 vector<VerificationNote>& notes
508 auto asset = reel_asset->asset();
509 auto const file = *asset->file();
511 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
512 stage ("Checking picture asset hash", file);
513 auto const r = verify_asset (dcp, reel_asset, progress);
515 case VerifyAssetResult::BAD:
517 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file
520 case VerifyAssetResult::CPL_PKL_DIFFER:
522 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file
530 stage ("Checking picture frame sizes", asset->file());
531 verify_picture_asset(reel_asset, file, start_frame, notes, progress);
533 /* Only flat/scope allowed by Bv2.1 */
535 asset->size() != Size(2048, 858) &&
536 asset->size() != Size(1998, 1080) &&
537 asset->size() != Size(4096, 1716) &&
538 asset->size() != Size(3996, 2160)) {
540 VerificationNote::Type::BV21_ERROR,
541 VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS,
542 String::compose("%1x%2", asset->size().width, asset->size().height),
547 /* Only 24, 25, 48fps allowed for 2K */
549 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
550 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
553 VerificationNote::Type::BV21_ERROR,
554 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
555 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
560 if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
561 /* Only 24fps allowed for 4K */
562 if (asset->edit_rate() != Fraction(24, 1)) {
564 VerificationNote::Type::BV21_ERROR,
565 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
566 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
571 /* Only 2D allowed for 4K */
572 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
574 VerificationNote::Type::BV21_ERROR,
575 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
576 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
588 boost::optional<string> subtitle_language;
589 boost::optional<int> audio_channels;
594 verify_main_sound_asset (
595 shared_ptr<const DCP> dcp,
596 shared_ptr<const ReelSoundAsset> reel_asset,
597 function<void (string, optional<boost::filesystem::path>)> stage,
598 function<void (float)> progress,
599 VerificationOptions options,
600 vector<VerificationNote>& notes,
604 auto asset = reel_asset->asset();
605 auto const file = *asset->file();
607 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
608 stage("Checking sound asset hash", file);
609 auto const r = verify_asset (dcp, reel_asset, progress);
611 case VerifyAssetResult::BAD:
612 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, file});
614 case VerifyAssetResult::CPL_PKL_DIFFER:
615 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, file});
622 if (!state.audio_channels) {
623 state.audio_channels = asset->channels();
624 } else if (*state.audio_channels != asset->channels()) {
625 notes.push_back({ VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS, file });
628 stage ("Checking sound asset metadata", file);
630 if (auto lang = asset->language()) {
631 verify_language_tag (*lang, notes);
633 if (asset->sampling_rate() != 48000) {
634 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), file});
640 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
642 /* XXX: is Language compulsory? */
643 if (reel_asset->language()) {
644 verify_language_tag (*reel_asset->language(), notes);
647 if (!reel_asset->entry_point()) {
648 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id() });
649 } else if (reel_asset->entry_point().get()) {
650 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
656 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
658 /* XXX: is Language compulsory? */
659 if (reel_asset->language()) {
660 verify_language_tag (*reel_asset->language(), notes);
663 if (!reel_asset->entry_point()) {
664 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
665 } else if (reel_asset->entry_point().get()) {
666 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
671 /** Verify stuff that is common to both subtitles and closed captions */
673 verify_smpte_timed_text_asset (
674 shared_ptr<const SMPTESubtitleAsset> asset,
675 optional<int64_t> reel_asset_duration,
676 vector<VerificationNote>& notes
679 if (asset->language()) {
680 verify_language_tag (*asset->language(), notes);
682 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
685 auto const size = filesystem::file_size(asset->file().get());
686 if (size > 115 * 1024 * 1024) {
688 { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file() }
692 /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
693 * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
695 auto fonts = asset->font_data ();
697 for (auto i: fonts) {
698 total_size += i.second.size();
700 if (total_size > 10 * 1024 * 1024) {
701 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
704 if (!asset->start_time()) {
705 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get() });
706 } else if (asset->start_time() != Time()) {
707 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() });
710 if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
713 VerificationNote::Type::BV21_ERROR,
714 VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
715 String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
722 /** Verify Interop subtitle / CCAP stuff */
724 verify_interop_text_asset(shared_ptr<const InteropSubtitleAsset> asset, vector<VerificationNote>& notes)
726 if (asset->subtitles().empty()) {
727 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_SUBTITLE, asset->id(), asset->file().get() });
729 auto const unresolved = asset->unresolved_fonts();
730 if (!unresolved.empty()) {
731 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_FONT, unresolved.front() });
736 /** Verify SMPTE subtitle-only stuff */
738 verify_smpte_subtitle_asset (
739 shared_ptr<const SMPTESubtitleAsset> asset,
740 vector<VerificationNote>& notes,
744 if (asset->language()) {
745 if (!state.subtitle_language) {
746 state.subtitle_language = *asset->language();
747 } else if (state.subtitle_language != *asset->language()) {
748 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
752 DCP_ASSERT (asset->resource_id());
753 auto xml_id = asset->xml_id();
755 if (asset->resource_id().get() != xml_id) {
756 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
759 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
760 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
763 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
766 if (asset->raw_xml()) {
767 /* Deluxe require this in their QC even if it seems never to be mentioned in any standard */
768 cxml::Document doc("SubtitleReel");
769 doc.read_string(*asset->raw_xml());
770 auto issue_date = doc.string_child("IssueDate");
771 std::regex reg("^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d$");
772 if (!std::regex_match(issue_date, reg)) {
773 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, issue_date});
779 /** Verify all subtitle stuff */
781 verify_subtitle_asset (
782 shared_ptr<const SubtitleAsset> asset,
783 optional<int64_t> reel_asset_duration,
784 function<void (string, optional<boost::filesystem::path>)> stage,
785 boost::filesystem::path xsd_dtd_directory,
786 vector<VerificationNote>& notes,
790 stage ("Checking subtitle XML", asset->file());
791 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
792 * gets passed through libdcp which may clean up and therefore hide errors.
794 if (asset->raw_xml()) {
795 validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
797 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
800 auto namespace_count = [](shared_ptr<const SubtitleAsset> asset, string root_node) {
801 cxml::Document doc(root_node);
802 doc.read_string(asset->raw_xml().get());
803 auto root = dynamic_cast<xmlpp::Element*>(doc.node())->cobj();
805 for (auto ns = root->nsDef; ns != nullptr; ns = ns->next) {
811 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
813 verify_interop_text_asset(interop, notes);
814 if (namespace_count(asset, "DCSubtitle") > 1) {
815 notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id() });
819 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
821 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
822 verify_smpte_subtitle_asset (smpte, notes, state);
823 /* This asset may be encrypted and in that case we'll have no raw_xml() */
824 if (asset->raw_xml() && namespace_count(asset, "SubtitleReel") > 1) {
825 notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id()});
831 /** Verify all closed caption stuff */
833 verify_closed_caption_asset (
834 shared_ptr<const SubtitleAsset> asset,
835 optional<int64_t> reel_asset_duration,
836 function<void (string, optional<boost::filesystem::path>)> stage,
837 boost::filesystem::path xsd_dtd_directory,
838 vector<VerificationNote>& notes
841 stage ("Checking closed caption XML", asset->file());
842 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
843 * gets passed through libdcp which may clean up and therefore hide errors.
845 auto raw_xml = asset->raw_xml();
847 validate_xml (*raw_xml, xsd_dtd_directory, notes);
848 if (raw_xml->size() > 256 * 1024) {
849 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
852 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
855 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
857 verify_interop_text_asset(interop, notes);
860 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
862 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
867 /** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes etc. */
870 verify_text_details (
871 dcp::Standard standard,
872 vector<shared_ptr<Reel>> reels,
874 vector<VerificationNote>& notes,
875 std::function<bool (shared_ptr<Reel>)> check,
876 std::function<optional<string> (shared_ptr<Reel>)> xml,
877 std::function<int64_t (shared_ptr<Reel>)> duration,
878 std::function<std::string (shared_ptr<Reel>)> id
881 /* end of last subtitle (in editable units) */
882 optional<int64_t> last_out;
883 auto too_short = false;
884 auto too_close = false;
885 auto too_early = false;
886 auto reel_overlap = false;
887 auto empty_text = false;
888 /* current reel start time (in editable units) */
889 int64_t reel_offset = 0;
890 optional<string> missing_load_font_id;
892 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool, bool&, vector<string>&)> parse;
894 parse = [&parse, &last_out, &too_short, &too_close, &too_early, &empty_text, &reel_offset, &missing_load_font_id](
895 cxml::ConstNodePtr node,
897 optional<Time> start_time,
901 vector<string>& font_ids
903 if (node->name() == "Subtitle") {
904 Time in (node->string_attribute("TimeIn"), tcr);
908 Time out (node->string_attribute("TimeOut"), tcr);
912 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
915 auto length = out - in;
916 if (length.as_editable_units_ceil(er) < 15) {
920 /* XXX: this feels dubious - is it really what Bv2.1 means? */
921 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
922 if (distance >= 0 && distance < 2) {
926 last_out = reel_offset + out.as_editable_units_floor(er);
927 } else if (node->name() == "Text") {
928 std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
929 if (!node->content().empty()) {
932 for (auto i: node->node_children()) {
933 if (node_has_content(i)) {
939 if (!node_has_content(node)) {
943 } else if (node->name() == "LoadFont") {
944 if (auto const id = node->optional_string_attribute("Id")) {
945 font_ids.push_back(*id);
946 } else if (auto const id = node->optional_string_attribute("ID")) {
947 font_ids.push_back(*id);
949 } else if (node->name() == "Font") {
950 if (auto const font_id = node->optional_string_attribute("Id")) {
951 if (std::find_if(font_ids.begin(), font_ids.end(), [font_id](string const& id) { return id == font_id; }) == font_ids.end()) {
952 missing_load_font_id = font_id;
956 for (auto i: node->node_children()) {
957 parse(i, tcr, start_time, er, first_reel, has_text, font_ids);
961 for (auto i = 0U; i < reels.size(); ++i) {
962 if (!check(reels[i])) {
966 auto reel_xml = xml(reels[i]);
968 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
972 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
973 * read in by libdcp's parser.
976 shared_ptr<cxml::Document> doc;
978 optional<Time> start_time;
980 case dcp::Standard::INTEROP:
981 doc = make_shared<cxml::Document>("DCSubtitle");
982 doc->read_string (*reel_xml);
984 case dcp::Standard::SMPTE:
985 doc = make_shared<cxml::Document>("SubtitleReel");
986 doc->read_string (*reel_xml);
987 tcr = doc->number_child<int>("TimeCodeRate");
988 if (auto start_time_string = doc->optional_string_child("StartTime")) {
989 start_time = Time(*start_time_string, tcr);
993 bool has_text = false;
994 vector<string> font_ids;
995 parse(doc, tcr, start_time, edit_rate, i == 0, has_text, font_ids);
996 auto end = reel_offset + duration(reels[i]);
997 if (last_out && *last_out > end) {
1002 if (standard == dcp::Standard::SMPTE && has_text && font_ids.empty()) {
1003 notes.push_back(dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT).set_id(id(reels[i])));
1007 if (last_out && *last_out > reel_offset) {
1008 reel_overlap = true;
1013 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
1019 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
1025 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
1031 VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
1037 VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_TEXT
1041 if (missing_load_font_id) {
1042 notes.push_back(dcp::VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT).set_id(*missing_load_font_id));
1049 verify_closed_caption_details (
1050 vector<shared_ptr<Reel>> reels,
1051 vector<VerificationNote>& notes
1054 std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
1055 find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
1056 for (auto i: node->node_children()) {
1057 if (i->name() == "Text") {
1058 text_or_image.push_back (i);
1060 find_text_or_image (i, text_or_image);
1065 auto mismatched_valign = false;
1066 auto incorrect_order = false;
1068 std::function<void (cxml::ConstNodePtr)> parse;
1069 parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
1070 if (node->name() == "Subtitle") {
1071 vector<cxml::ConstNodePtr> text_or_image;
1072 find_text_or_image (node, text_or_image);
1073 optional<string> last_valign;
1074 optional<float> last_vpos;
1075 for (auto i: text_or_image) {
1076 auto valign = i->optional_string_attribute("VAlign");
1078 valign = i->optional_string_attribute("Valign").get_value_or("center");
1080 auto vpos = i->optional_number_attribute<float>("VPosition");
1082 vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
1086 if (*last_valign != valign) {
1087 mismatched_valign = true;
1090 last_valign = valign;
1092 if (!mismatched_valign) {
1094 if (*last_valign == "top" || *last_valign == "center") {
1095 if (*vpos < *last_vpos) {
1096 incorrect_order = true;
1099 if (*vpos > *last_vpos) {
1100 incorrect_order = true;
1109 for (auto i: node->node_children()) {
1114 for (auto reel: reels) {
1115 for (auto ccap: reel->closed_captions()) {
1116 auto reel_xml = ccap->asset()->raw_xml();
1118 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
1122 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1123 * read in by libdcp's parser.
1126 shared_ptr<cxml::Document> doc;
1128 optional<Time> start_time;
1130 doc = make_shared<cxml::Document>("SubtitleReel");
1131 doc->read_string (*reel_xml);
1133 doc = make_shared<cxml::Document>("DCSubtitle");
1134 doc->read_string (*reel_xml);
1140 if (mismatched_valign) {
1142 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN,
1146 if (incorrect_order) {
1148 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING,
1154 struct LinesCharactersResult
1156 bool warning_length_exceeded = false;
1157 bool error_length_exceeded = false;
1158 bool line_count_exceeded = false;
1164 verify_text_lines_and_characters (
1165 shared_ptr<SubtitleAsset> asset,
1168 LinesCharactersResult* result
1174 Event (Time time_, float position_, int characters_)
1176 , position (position_)
1177 , characters (characters_)
1180 Event (Time time_, shared_ptr<Event> start_)
1186 int position; //< position from 0 at top of screen to 100 at bottom
1188 shared_ptr<Event> start;
1191 vector<shared_ptr<Event>> events;
1193 auto position = [](shared_ptr<const SubtitleString> sub) {
1194 switch (sub->v_align()) {
1196 return lrintf(sub->v_position() * 100);
1197 case VAlign::CENTER:
1198 return lrintf((0.5f + sub->v_position()) * 100);
1199 case VAlign::BOTTOM:
1200 return lrintf((1.0f - sub->v_position()) * 100);
1206 for (auto j: asset->subtitles()) {
1207 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1209 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1210 events.push_back(in);
1211 events.push_back(make_shared<Event>(text->out(), in));
1215 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1216 return a->time < b->time;
1219 map<int, int> current;
1220 for (auto i: events) {
1221 if (current.size() > 3) {
1222 result->line_count_exceeded = true;
1224 for (auto j: current) {
1225 if (j.second > warning_length) {
1226 result->warning_length_exceeded = true;
1228 if (j.second > error_length) {
1229 result->error_length_exceeded = true;
1234 /* end of a subtitle */
1235 DCP_ASSERT (current.find(i->start->position) != current.end());
1236 if (current[i->start->position] == i->start->characters) {
1237 current.erase(i->start->position);
1239 current[i->start->position] -= i->start->characters;
1242 /* start of a subtitle */
1243 if (current.find(i->position) == current.end()) {
1244 current[i->position] = i->characters;
1246 current[i->position] += i->characters;
1255 verify_text_details(dcp::Standard standard, vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1257 if (reels.empty()) {
1261 if (reels[0]->main_subtitle()) {
1262 verify_text_details(standard, reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1263 [](shared_ptr<Reel> reel) {
1264 return static_cast<bool>(reel->main_subtitle());
1266 [](shared_ptr<Reel> reel) {
1267 return reel->main_subtitle()->asset()->raw_xml();
1269 [](shared_ptr<Reel> reel) {
1270 return reel->main_subtitle()->actual_duration();
1272 [](shared_ptr<Reel> reel) {
1273 return reel->main_subtitle()->id();
1278 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1279 verify_text_details(standard, reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1280 [i](shared_ptr<Reel> reel) {
1281 return i < reel->closed_captions().size();
1283 [i](shared_ptr<Reel> reel) {
1284 return reel->closed_captions()[i]->asset()->raw_xml();
1286 [i](shared_ptr<Reel> reel) {
1287 return reel->closed_captions()[i]->actual_duration();
1289 [i](shared_ptr<Reel> reel) {
1290 return reel->closed_captions()[i]->id();
1295 verify_closed_caption_details (reels, notes);
1300 verify_extension_metadata(shared_ptr<const CPL> cpl, vector<VerificationNote>& notes)
1302 DCP_ASSERT (cpl->file());
1303 cxml::Document doc ("CompositionPlaylist");
1304 doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1306 auto missing = false;
1309 if (auto reel_list = doc.node_child("ReelList")) {
1310 auto reels = reel_list->node_children("Reel");
1311 if (!reels.empty()) {
1312 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1313 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1314 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1316 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1317 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1321 if (auto name = extension->optional_node_child("Name")) {
1322 if (name->content() != "Application") {
1323 malformed = "<Name> should be 'Application'";
1326 if (auto property_list = extension->optional_node_child("PropertyList")) {
1327 if (auto property = property_list->optional_node_child("Property")) {
1328 if (auto name = property->optional_node_child("Name")) {
1329 if (name->content() != "DCP Constraints Profile") {
1330 malformed = "<Name> property should be 'DCP Constraints Profile'";
1333 if (auto value = property->optional_node_child("Value")) {
1334 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1335 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1350 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1351 } else if (!malformed.empty()) {
1352 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1358 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1360 vector<string> encrypted;
1361 for (auto i: dcp->cpls()) {
1362 for (auto j: i->reel_file_assets()) {
1363 if (j->asset_ref().resolved()) {
1364 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1365 if (mxf && mxf->encrypted()) {
1366 encrypted.push_back(j->asset_ref().id());
1372 for (auto i: pkl->assets()) {
1373 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1385 shared_ptr<const DCP> dcp,
1386 shared_ptr<const CPL> cpl,
1387 shared_ptr<const Reel> reel,
1388 int64_t start_frame,
1389 optional<dcp::Size> main_picture_active_area,
1390 function<void (string, optional<boost::filesystem::path>)> stage,
1391 boost::filesystem::path xsd_dtd_directory,
1392 function<void (float)> progress,
1393 VerificationOptions options,
1394 vector<VerificationNote>& notes,
1396 bool* have_main_subtitle,
1397 bool* have_no_main_subtitle,
1398 size_t* most_closed_captions,
1399 size_t* fewest_closed_captions,
1400 map<Marker, Time>* markers_seen
1403 for (auto i: reel->assets()) {
1404 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1405 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1407 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1408 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1410 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1411 if (i->encryptable() && !file_asset->hash()) {
1412 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1416 if (dcp->standard() == Standard::SMPTE) {
1417 boost::optional<int64_t> duration;
1418 for (auto i: reel->assets()) {
1420 duration = i->actual_duration();
1421 } else if (*duration != i->actual_duration()) {
1422 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1428 if (reel->main_picture()) {
1429 /* Check reel stuff */
1430 auto const frame_rate = reel->main_picture()->frame_rate();
1431 if (frame_rate.denominator != 1 ||
1432 (frame_rate.numerator != 24 &&
1433 frame_rate.numerator != 25 &&
1434 frame_rate.numerator != 30 &&
1435 frame_rate.numerator != 48 &&
1436 frame_rate.numerator != 50 &&
1437 frame_rate.numerator != 60 &&
1438 frame_rate.numerator != 96)) {
1440 VerificationNote::Type::ERROR,
1441 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1442 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1446 if (reel->main_picture()->asset_ref().resolved()) {
1447 verify_main_picture_asset(dcp, reel->main_picture(), start_frame, stage, progress, options, notes);
1448 auto const asset_size = reel->main_picture()->asset()->size();
1449 if (main_picture_active_area) {
1450 if (main_picture_active_area->width > asset_size.width) {
1452 VerificationNote::Type::ERROR,
1453 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1454 String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1458 if (main_picture_active_area->height > asset_size.height) {
1460 VerificationNote::Type::ERROR,
1461 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1462 String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1471 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1472 verify_main_sound_asset(dcp, reel->main_sound(), stage, progress, options, notes, state);
1475 if (reel->main_subtitle()) {
1476 verify_main_subtitle_reel(reel->main_subtitle(), notes);
1477 if (reel->main_subtitle()->asset_ref().resolved()) {
1478 verify_subtitle_asset(reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, xsd_dtd_directory, notes, state);
1480 *have_main_subtitle = true;
1482 *have_no_main_subtitle = true;
1485 for (auto i: reel->closed_captions()) {
1486 verify_closed_caption_reel(i, notes);
1487 if (i->asset_ref().resolved()) {
1488 verify_closed_caption_asset(i->asset(), i->duration(), stage, xsd_dtd_directory, notes);
1492 if (reel->main_markers()) {
1493 for (auto const& i: reel->main_markers()->get()) {
1494 markers_seen->insert(i);
1496 if (reel->main_markers()->entry_point()) {
1497 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_ENTRY_POINT});
1499 if (reel->main_markers()->duration()) {
1500 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_DURATION});
1504 *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1505 *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1513 shared_ptr<const DCP> dcp,
1514 shared_ptr<const CPL> cpl,
1515 function<void (string, optional<boost::filesystem::path>)> stage,
1516 boost::filesystem::path xsd_dtd_directory,
1517 function<void (float)> progress,
1518 VerificationOptions options,
1519 vector<VerificationNote>& notes,
1523 stage("Checking CPL", cpl->file());
1524 validate_xml(cpl->file().get(), xsd_dtd_directory, notes);
1526 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1527 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1530 for (auto const& i: cpl->additional_subtitle_languages()) {
1531 verify_language_tag(i, notes);
1534 if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1535 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1536 * of the approved ones.
1538 auto all = ContentKind::all();
1539 auto name = cpl->content_kind().name();
1540 transform(name.begin(), name.end(), name.begin(), ::tolower);
1541 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1542 if (iter == all.end()) {
1543 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name()});
1547 if (cpl->release_territory()) {
1548 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") {
1549 auto terr = cpl->release_territory().get();
1550 /* Must be a valid region tag, or "001" */
1552 LanguageTag::RegionSubtag test(terr);
1554 if (terr != "001") {
1555 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1561 for (auto version: cpl->content_versions()) {
1562 if (version.label_text.empty()) {
1564 dcp::VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT, cpl->file().get()).set_id(cpl->id())
1570 if (dcp->standard() == Standard::SMPTE) {
1571 if (!cpl->annotation_text()) {
1572 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1573 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1574 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1578 for (auto i: dcp->pkls()) {
1579 /* Check that the CPL's hash corresponds to the PKL */
1580 optional<string> h = i->hash(cpl->id());
1581 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1582 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1585 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1586 optional<string> required_annotation_text;
1587 for (auto j: i->assets()) {
1588 /* See if this is a CPL */
1589 for (auto k: dcp->cpls()) {
1590 if (j->id() == k->id()) {
1591 if (!required_annotation_text) {
1592 /* First CPL we have found; this is the required AnnotationText unless we find another */
1593 required_annotation_text = cpl->content_title_text();
1595 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1596 required_annotation_text = boost::none;
1602 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1603 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1607 /* set to true if any reel has a MainSubtitle */
1608 auto have_main_subtitle = false;
1609 /* set to true if any reel has no MainSubtitle */
1610 auto have_no_main_subtitle = false;
1611 /* fewest number of closed caption assets seen in a reel */
1612 size_t fewest_closed_captions = SIZE_MAX;
1613 /* most number of closed caption assets seen in a reel */
1614 size_t most_closed_captions = 0;
1615 map<Marker, Time> markers_seen;
1617 auto const main_picture_active_area = cpl->main_picture_active_area();
1618 if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1620 VerificationNote::Type::ERROR,
1621 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1622 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1626 if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1628 VerificationNote::Type::ERROR,
1629 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1630 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1636 for (auto reel: cpl->reels()) {
1637 stage("Checking reel", optional<boost::filesystem::path>());
1643 main_picture_active_area,
1650 &have_main_subtitle,
1651 &have_no_main_subtitle,
1652 &most_closed_captions,
1653 &fewest_closed_captions,
1656 frame += reel->duration();
1659 verify_text_details(dcp->standard().get_value_or(dcp::Standard::SMPTE), cpl->reels(), notes);
1661 if (dcp->standard() == Standard::SMPTE) {
1662 if (auto msc = cpl->main_sound_configuration()) {
1663 if (state.audio_channels && msc->channels() != *state.audio_channels) {
1665 VerificationNote::Type::ERROR,
1666 VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION,
1667 String::compose("MainSoundConfiguration has %1 channels but sound assets have %2", msc->channels(), *state.audio_channels),
1673 if (have_main_subtitle && have_no_main_subtitle) {
1674 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1677 if (fewest_closed_captions != most_closed_captions) {
1678 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1681 if (cpl->content_kind() == ContentKind::FEATURE) {
1682 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1683 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1685 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1686 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1690 auto ffoc = markers_seen.find(Marker::FFOC);
1691 if (ffoc == markers_seen.end()) {
1692 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1693 } else if (ffoc->second.e != 1) {
1694 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1697 auto lfoc = markers_seen.find(Marker::LFOC);
1698 if (lfoc == markers_seen.end()) {
1699 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1701 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1702 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1703 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1707 LinesCharactersResult result;
1708 for (auto reel: cpl->reels()) {
1709 if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1710 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1714 if (result.line_count_exceeded) {
1715 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1717 if (result.error_length_exceeded) {
1718 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1719 } else if (result.warning_length_exceeded) {
1720 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1723 result = LinesCharactersResult();
1724 for (auto reel: cpl->reels()) {
1725 for (auto i: reel->closed_captions()) {
1727 verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1732 if (result.line_count_exceeded) {
1733 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1735 if (result.error_length_exceeded) {
1736 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1739 if (!cpl->read_composition_metadata()) {
1740 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1741 } else if (!cpl->version_number()) {
1742 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1745 verify_extension_metadata(cpl, notes);
1747 if (cpl->any_encrypted()) {
1748 cxml::Document doc("CompositionPlaylist");
1749 DCP_ASSERT(cpl->file());
1750 doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1751 if (!doc.optional_node_child("Signature")) {
1752 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1762 shared_ptr<const DCP> dcp,
1763 shared_ptr<const PKL> pkl,
1764 boost::filesystem::path xsd_dtd_directory,
1765 vector<VerificationNote>& notes
1768 validate_xml(pkl->file().get(), xsd_dtd_directory, notes);
1770 if (pkl_has_encrypted_assets(dcp, pkl)) {
1771 cxml::Document doc("PackingList");
1772 doc.read_file(dcp::filesystem::fix_long_path(pkl->file().get()));
1773 if (!doc.optional_node_child("Signature")) {
1774 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1778 set<string> uuid_set;
1779 for (auto asset: pkl->assets()) {
1780 if (!uuid_set.insert(asset->id()).second) {
1781 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get()});
1792 shared_ptr<const DCP> dcp,
1793 boost::filesystem::path xsd_dtd_directory,
1794 vector<VerificationNote>& notes
1797 auto asset_map = dcp->asset_map();
1798 DCP_ASSERT(asset_map);
1800 validate_xml(asset_map->file().get(), xsd_dtd_directory, notes);
1802 set<string> uuid_set;
1803 for (auto const& asset: asset_map->assets()) {
1804 if (!uuid_set.insert(asset.id()).second) {
1805 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get()});
1812 vector<VerificationNote>
1814 vector<boost::filesystem::path> directories,
1815 vector<dcp::DecryptedKDM> kdms,
1816 function<void (string, optional<boost::filesystem::path>)> stage,
1817 function<void (float)> progress,
1818 VerificationOptions options,
1819 optional<boost::filesystem::path> xsd_dtd_directory
1822 if (!xsd_dtd_directory) {
1823 xsd_dtd_directory = resources_directory() / "xsd";
1825 *xsd_dtd_directory = filesystem::canonical(*xsd_dtd_directory);
1827 vector<VerificationNote> notes;
1830 vector<shared_ptr<DCP>> dcps;
1831 for (auto i: directories) {
1832 dcps.push_back (make_shared<DCP>(i));
1835 for (auto dcp: dcps) {
1836 stage ("Checking DCP", dcp->directory());
1837 bool carry_on = true;
1839 dcp->read (¬es, true);
1840 } catch (MissingAssetmapError& e) {
1841 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1843 } catch (ReadError& e) {
1844 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1845 } catch (XMLError& e) {
1846 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1847 } catch (MXFFileError& e) {
1848 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1849 } catch (BadURNUUIDError& e) {
1850 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1851 } catch (cxml::Error& e) {
1852 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1859 if (dcp->standard() != Standard::SMPTE) {
1860 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1863 for (auto kdm: kdms) {
1867 for (auto cpl: dcp->cpls()) {
1880 for (auto pkl: dcp->pkls()) {
1881 stage("Checking PKL", pkl->file());
1882 verify_pkl(dcp, pkl, *xsd_dtd_directory, notes);
1885 if (dcp->asset_map_file()) {
1886 stage("Checking ASSETMAP", dcp->asset_map_file().get());
1887 verify_assetmap(dcp, *xsd_dtd_directory, notes);
1889 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1898 dcp::note_to_string (VerificationNote note)
1900 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1902 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1903 * not "ClosedCaption assets must have an <EntryPoint> tag."
1905 * It's OK to use XML tag names where they are clear.
1906 * If both ID and filename are available, use only the ID.
1907 * End messages with a full stop.
1908 * Messages should not mention whether or not their errors are a part of Bv2.1.
1910 switch (note.code()) {
1911 case VerificationNote::Code::FAILED_READ:
1912 return *note.note();
1913 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1914 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1915 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1916 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1917 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1918 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1919 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1920 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1921 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1922 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1923 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1924 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1925 case VerificationNote::Code::EMPTY_ASSET_PATH:
1926 return "The asset map contains an empty asset path.";
1927 case VerificationNote::Code::MISSING_ASSET:
1928 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1929 case VerificationNote::Code::MISMATCHED_STANDARD:
1930 return "The DCP contains both SMPTE and Interop parts.";
1931 case VerificationNote::Code::INVALID_XML:
1932 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1933 case VerificationNote::Code::MISSING_ASSETMAP:
1934 return "No valid ASSETMAP or ASSETMAP.xml was found.";
1935 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1936 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1937 case VerificationNote::Code::INVALID_DURATION:
1938 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1939 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1940 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());
1941 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1942 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());
1943 case VerificationNote::Code::EXTERNAL_ASSET:
1944 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());
1945 case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1946 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1947 case VerificationNote::Code::INVALID_STANDARD:
1948 return "This DCP does not use the SMPTE standard.";
1949 case VerificationNote::Code::INVALID_LANGUAGE:
1950 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1951 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1952 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1953 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1954 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1955 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1956 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1957 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1958 return "3D 4K DCPs are not allowed.";
1959 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1960 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1961 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1962 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1963 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1964 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());
1965 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1966 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1967 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1968 return "Some subtitle assets have different <Language> tags than others";
1969 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1970 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1971 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1972 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1973 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1974 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1975 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1976 return "At least one subtitle lasts less than 15 frames.";
1977 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1978 return "At least one pair of subtitles is separated by less than 2 frames.";
1979 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1980 return "At least one subtitle extends outside of its reel.";
1981 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1982 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1983 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1984 return "There are more than 52 characters in at least one subtitle line.";
1985 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1986 return "There are more than 79 characters in at least one subtitle line.";
1987 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1988 return "There are more than 3 closed caption lines in at least one place.";
1989 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1990 return "There are more than 32 characters in at least one closed caption line.";
1991 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1992 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1993 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1994 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1995 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1996 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.note().get());
1997 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1998 return "All assets in a reel do not have the same duration.";
1999 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
2000 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
2001 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
2002 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
2003 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
2004 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
2005 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
2006 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
2007 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
2008 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
2009 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
2010 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
2011 case VerificationNote::Code::MISSING_HASH:
2012 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
2013 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
2014 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
2015 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
2016 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
2017 case VerificationNote::Code::MISSING_FFOC:
2018 return "There should be a FFOC (first frame of content) marker.";
2019 case VerificationNote::Code::MISSING_LFOC:
2020 return "There should be a LFOC (last frame of content) marker.";
2021 case VerificationNote::Code::INCORRECT_FFOC:
2022 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
2023 case VerificationNote::Code::INCORRECT_LFOC:
2024 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
2025 case VerificationNote::Code::MISSING_CPL_METADATA:
2026 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
2027 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
2028 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
2029 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
2030 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
2031 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
2032 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
2033 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
2034 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
2035 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
2036 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
2037 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
2038 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
2039 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
2040 return "Some assets are encrypted but some are not.";
2041 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
2042 return String::compose(
2043 "Frame %1 (timecode %2) has an invalid JPEG2000 codestream (%2).",
2045 dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
2048 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
2049 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
2050 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
2051 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
2052 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
2053 return "The JPEG2000 tile size is not the same as the image size.";
2054 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
2055 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
2056 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
2057 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
2058 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
2059 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
2060 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
2061 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
2062 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
2063 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
2064 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
2065 return "POC marker found outside main header.";
2066 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
2067 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
2068 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
2069 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
2070 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
2071 return "No TLM marker was found in a JPEG2000 codestream.";
2072 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
2073 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
2074 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
2075 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
2076 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
2078 vector<string> parts;
2079 boost::split (parts, note.note().get(), boost::is_any_of(" "));
2080 DCP_ASSERT (parts.size() == 2);
2081 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]);
2083 case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
2084 return "Some aspect of this DCP could not be checked because it is encrypted.";
2085 case VerificationNote::Code::EMPTY_TEXT:
2086 return "There is an empty <Text> node in a subtitle or closed caption.";
2087 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
2088 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
2089 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
2090 return "Some closed captions are not listed in the order of their vertical position.";
2091 case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
2092 return "There is an <EntryPoint> node inside a <MainMarkers>.";
2093 case VerificationNote::Code::UNEXPECTED_DURATION:
2094 return "There is an <Duration> node inside a <MainMarkers>.";
2095 case VerificationNote::Code::INVALID_CONTENT_KIND:
2096 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
2097 case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
2098 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
2099 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
2100 return String::compose("The PKL %1 has more than one asset with the same ID.", note.note().get());
2101 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
2102 return String::compose("The ASSETMAP %1 has more than one asset with the same ID.", note.note().get());
2103 case VerificationNote::Code::MISSING_SUBTITLE:
2104 return String::compose("The subtitle asset %1 has no subtitles.", note.note().get());
2105 case VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
2106 return String::compose("<IssueDate> has an invalid value: %1", note.note().get());
2107 case VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS:
2108 return String::compose("The sound assets do not all have the same channel count; the first to differ is %1", note.file()->filename());
2109 case VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION:
2110 return String::compose("<MainSoundConfiguration> has an invalid value: %1", note.note().get());
2111 case VerificationNote::Code::MISSING_FONT:
2112 return String::compose("The font file for font ID \"%1\" was not found, or was not referred to in the ASSETMAP.", note.note().get());
2113 case VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE:
2114 return String::compose(
2115 "Frame %1 has an image component that is too large (component %2 is %3 bytes in size).",
2116 note.frame().get(), note.component().get(), note.size().get()
2118 case VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT:
2119 return String::compose("The XML in the subtitle asset %1 has more than one namespace declaration.", note.note().get());
2120 case VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT:
2121 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());
2122 case VerificationNote::Code::MISSING_LOAD_FONT:
2123 return String::compose("The SMPTE subtitle asset %1 has <Text> nodes but no <LoadFont> node", note.id().get());
2124 case VerificationNote::Code::MISMATCHED_ASSET_MAP_ID:
2125 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());
2126 case VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT:
2127 return String::compose("The <LabelText> in a <ContentVersion> in CPL %1 is empty", note.id().get());
2135 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2137 return a.type() == b.type() &&
2138 a.code() == b.code() &&
2139 a.note() == b.note() &&
2140 a.file() == b.file() &&
2141 a.line() == b.line() &&
2142 a.frame() == b.frame() &&
2143 a.component() == b.component() &&
2144 a.size() == b.size() &&
2146 a.other_id() == b.other_id() &&
2147 a.frame_rate() == b.frame_rate();
2152 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2154 if (a.type() != b.type()) {
2155 return a.type() < b.type();
2158 if (a.code() != b.code()) {
2159 return a.code() < b.code();
2162 if (a.note() != b.note()) {
2163 return a.note().get_value_or("") < b.note().get_value_or("");
2166 if (a.file() != b.file()) {
2167 return a.file().get_value_or("") < b.file().get_value_or("");
2170 if (a.line() != b.line()) {
2171 return a.line().get_value_or(0) < b.line().get_value_or(0);
2174 if (a.frame() != b.frame()) {
2175 return a.frame().get_value_or(0) < b.frame().get_value_or(0);
2178 if (a.component() != b.component()) {
2179 return a.component().get_value_or(0) < b.component().get_value_or(0);
2182 if (a.size() != b.size()) {
2183 return a.size().get_value_or(0) < b.size().get_value_or(0);
2186 if (a.id() != b.id()) {
2187 return a.id().get_value_or("") < b.id().get_value_or("");
2190 return a.other_id().get_value_or("") < b.other_id().get_value_or("");
2195 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2197 s << note_to_string (note);
2199 s << " [" << note.note().get() << "]";
2202 s << " [" << note.file().get() << "]";
2205 s << " [" << note.line().get() << "]";