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 auto asset = dynamic_pointer_cast<PictureAsset>(reel_file_asset->asset_ref().asset());
444 auto const duration = asset->intrinsic_duration ();
446 auto check_and_add = [¬es](vector<VerificationNote> const& j2k_notes) {
447 for (auto i: j2k_notes) {
448 if (find(notes.begin(), notes.end(), i) == notes.end()) {
454 int const max_frame = rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
455 int const risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
457 auto check_frame_size = [max_frame, risky_frame, file, start_frame](int index, int size, int frame_rate, vector<VerificationNote>& notes) {
458 if (size > max_frame) {
461 VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
462 ).set_frame(start_frame + index).set_frame_rate(frame_rate)
464 } else if (size > risky_frame) {
467 VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
468 ).set_frame(start_frame + index).set_frame_rate(frame_rate)
473 if (auto mono_asset = dynamic_pointer_cast<MonoPictureAsset>(reel_file_asset->asset_ref().asset())) {
474 auto reader = mono_asset->start_read ();
475 for (int64_t i = 0; i < duration; ++i) {
476 auto frame = reader->get_frame (i);
477 check_frame_size(i, frame->size(), mono_asset->frame_rate().numerator, notes);
478 if (!mono_asset->encrypted() || mono_asset->key()) {
479 vector<VerificationNote> j2k_notes;
480 verify_j2k(frame, start_frame, i, mono_asset->frame_rate().numerator, j2k_notes);
481 check_and_add (j2k_notes);
483 progress (float(i) / duration);
485 } else if (auto stereo_asset = dynamic_pointer_cast<StereoPictureAsset>(asset)) {
486 auto reader = stereo_asset->start_read ();
487 for (int64_t i = 0; i < duration; ++i) {
488 auto frame = reader->get_frame (i);
489 check_frame_size(i, frame->left()->size(), stereo_asset->frame_rate().numerator, notes);
490 check_frame_size(i, frame->right()->size(), stereo_asset->frame_rate().numerator, notes);
491 if (!stereo_asset->encrypted() || stereo_asset->key()) {
492 vector<VerificationNote> j2k_notes;
493 verify_j2k(frame->left(), start_frame, i, stereo_asset->frame_rate().numerator, j2k_notes);
494 verify_j2k(frame->right(), start_frame, i, stereo_asset->frame_rate().numerator, j2k_notes);
495 check_and_add (j2k_notes);
497 progress (float(i) / duration);
505 verify_main_picture_asset (
506 shared_ptr<const DCP> dcp,
507 shared_ptr<const ReelPictureAsset> reel_asset,
509 function<void (string, optional<boost::filesystem::path>)> stage,
510 function<void (float)> progress,
511 VerificationOptions options,
512 vector<VerificationNote>& notes
515 auto asset = reel_asset->asset();
516 auto const file = *asset->file();
518 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
519 stage ("Checking picture asset hash", file);
520 auto const r = verify_asset (dcp, reel_asset, progress);
522 case VerifyAssetResult::BAD:
524 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file
527 case VerifyAssetResult::CPL_PKL_DIFFER:
529 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file
537 stage ("Checking picture frame sizes", asset->file());
538 verify_picture_asset(reel_asset, file, start_frame, notes, progress);
540 /* Only flat/scope allowed by Bv2.1 */
542 asset->size() != Size(2048, 858) &&
543 asset->size() != Size(1998, 1080) &&
544 asset->size() != Size(4096, 1716) &&
545 asset->size() != Size(3996, 2160)) {
547 VerificationNote::Type::BV21_ERROR,
548 VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS,
549 String::compose("%1x%2", asset->size().width, asset->size().height),
554 /* Only 24, 25, 48fps allowed for 2K */
556 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
557 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
560 VerificationNote::Type::BV21_ERROR,
561 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
562 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
567 if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
568 /* Only 24fps allowed for 4K */
569 if (asset->edit_rate() != Fraction(24, 1)) {
571 VerificationNote::Type::BV21_ERROR,
572 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
573 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
578 /* Only 2D allowed for 4K */
579 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
581 VerificationNote::Type::BV21_ERROR,
582 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
583 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
595 boost::optional<string> subtitle_language;
596 boost::optional<int> audio_channels;
601 verify_main_sound_asset (
602 shared_ptr<const DCP> dcp,
603 shared_ptr<const ReelSoundAsset> reel_asset,
604 function<void (string, optional<boost::filesystem::path>)> stage,
605 function<void (float)> progress,
606 VerificationOptions options,
607 vector<VerificationNote>& notes,
611 auto asset = reel_asset->asset();
612 auto const file = *asset->file();
614 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
615 stage("Checking sound asset hash", file);
616 auto const r = verify_asset (dcp, reel_asset, progress);
618 case VerifyAssetResult::BAD:
619 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, file});
621 case VerifyAssetResult::CPL_PKL_DIFFER:
622 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, file});
629 if (!state.audio_channels) {
630 state.audio_channels = asset->channels();
631 } else if (*state.audio_channels != asset->channels()) {
632 notes.push_back({ VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS, file });
635 stage ("Checking sound asset metadata", file);
637 if (auto lang = asset->language()) {
638 verify_language_tag (*lang, notes);
640 if (asset->sampling_rate() != 48000) {
641 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), file});
647 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
649 /* XXX: is Language compulsory? */
650 if (reel_asset->language()) {
651 verify_language_tag (*reel_asset->language(), notes);
654 if (!reel_asset->entry_point()) {
655 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id() });
656 } else if (reel_asset->entry_point().get()) {
657 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
663 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
665 /* XXX: is Language compulsory? */
666 if (reel_asset->language()) {
667 verify_language_tag (*reel_asset->language(), notes);
670 if (!reel_asset->entry_point()) {
671 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
672 } else if (reel_asset->entry_point().get()) {
673 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
678 /** Verify stuff that is common to both subtitles and closed captions */
680 verify_smpte_timed_text_asset (
681 shared_ptr<const SMPTESubtitleAsset> asset,
682 optional<int64_t> reel_asset_duration,
683 vector<VerificationNote>& notes
686 if (asset->language()) {
687 verify_language_tag (*asset->language(), notes);
689 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
692 auto const size = filesystem::file_size(asset->file().get());
693 if (size > 115 * 1024 * 1024) {
695 { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file() }
699 /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
700 * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
702 auto fonts = asset->font_data ();
704 for (auto i: fonts) {
705 total_size += i.second.size();
707 if (total_size > 10 * 1024 * 1024) {
708 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
711 if (!asset->start_time()) {
712 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get() });
713 } else if (asset->start_time() != Time()) {
714 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() });
717 if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
720 VerificationNote::Type::BV21_ERROR,
721 VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
722 String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
729 /** Verify Interop subtitle / CCAP stuff */
731 verify_interop_text_asset(shared_ptr<const InteropSubtitleAsset> asset, vector<VerificationNote>& notes)
733 if (asset->subtitles().empty()) {
734 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_SUBTITLE, asset->id(), asset->file().get() });
736 auto const unresolved = asset->unresolved_fonts();
737 if (!unresolved.empty()) {
738 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_FONT, unresolved.front() });
743 /** Verify SMPTE subtitle-only stuff */
745 verify_smpte_subtitle_asset (
746 shared_ptr<const SMPTESubtitleAsset> asset,
747 vector<VerificationNote>& notes,
751 if (asset->language()) {
752 if (!state.subtitle_language) {
753 state.subtitle_language = *asset->language();
754 } else if (state.subtitle_language != *asset->language()) {
755 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
759 DCP_ASSERT (asset->resource_id());
760 auto xml_id = asset->xml_id();
762 if (asset->resource_id().get() != xml_id) {
763 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
766 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
767 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
770 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
773 if (asset->raw_xml()) {
774 /* Deluxe require this in their QC even if it seems never to be mentioned in any standard */
775 cxml::Document doc("SubtitleReel");
776 doc.read_string(*asset->raw_xml());
777 auto issue_date = doc.string_child("IssueDate");
778 std::regex reg("^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d$");
779 if (!std::regex_match(issue_date, reg)) {
780 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, issue_date});
786 /** Verify all subtitle stuff */
788 verify_subtitle_asset (
789 shared_ptr<const SubtitleAsset> asset,
790 optional<int64_t> reel_asset_duration,
791 function<void (string, optional<boost::filesystem::path>)> stage,
792 boost::filesystem::path xsd_dtd_directory,
793 vector<VerificationNote>& notes,
797 stage ("Checking subtitle XML", asset->file());
798 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
799 * gets passed through libdcp which may clean up and therefore hide errors.
801 if (asset->raw_xml()) {
802 validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
804 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
807 auto namespace_count = [](shared_ptr<const SubtitleAsset> asset, string root_node) {
808 cxml::Document doc(root_node);
809 doc.read_string(asset->raw_xml().get());
810 auto root = dynamic_cast<xmlpp::Element*>(doc.node())->cobj();
812 for (auto ns = root->nsDef; ns != nullptr; ns = ns->next) {
818 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
820 verify_interop_text_asset(interop, notes);
821 if (namespace_count(asset, "DCSubtitle") > 1) {
822 notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id() });
826 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
828 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
829 verify_smpte_subtitle_asset (smpte, notes, state);
830 /* This asset may be encrypted and in that case we'll have no raw_xml() */
831 if (asset->raw_xml() && namespace_count(asset, "SubtitleReel") > 1) {
832 notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id()});
838 /** Verify all closed caption stuff */
840 verify_closed_caption_asset (
841 shared_ptr<const SubtitleAsset> asset,
842 optional<int64_t> reel_asset_duration,
843 function<void (string, optional<boost::filesystem::path>)> stage,
844 boost::filesystem::path xsd_dtd_directory,
845 vector<VerificationNote>& notes
848 stage ("Checking closed caption XML", asset->file());
849 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
850 * gets passed through libdcp which may clean up and therefore hide errors.
852 auto raw_xml = asset->raw_xml();
854 validate_xml (*raw_xml, xsd_dtd_directory, notes);
855 if (raw_xml->size() > 256 * 1024) {
856 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
859 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
862 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
864 verify_interop_text_asset(interop, notes);
867 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
869 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
874 /** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes etc. */
877 verify_text_details (
878 dcp::Standard standard,
879 vector<shared_ptr<Reel>> reels,
881 vector<VerificationNote>& notes,
882 std::function<bool (shared_ptr<Reel>)> check,
883 std::function<optional<string> (shared_ptr<Reel>)> xml,
884 std::function<int64_t (shared_ptr<Reel>)> duration,
885 std::function<std::string (shared_ptr<Reel>)> id
888 /* end of last subtitle (in editable units) */
889 optional<int64_t> last_out;
890 auto too_short = false;
891 auto too_close = false;
892 auto too_early = false;
893 auto reel_overlap = false;
894 auto empty_text = false;
895 /* current reel start time (in editable units) */
896 int64_t reel_offset = 0;
897 optional<string> missing_load_font_id;
899 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool, bool&, vector<string>&)> parse;
901 parse = [&parse, &last_out, &too_short, &too_close, &too_early, &empty_text, &reel_offset, &missing_load_font_id](
902 cxml::ConstNodePtr node,
904 optional<Time> start_time,
908 vector<string>& font_ids
910 if (node->name() == "Subtitle") {
911 Time in (node->string_attribute("TimeIn"), tcr);
915 Time out (node->string_attribute("TimeOut"), tcr);
919 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
922 auto length = out - in;
923 if (length.as_editable_units_ceil(er) < 15) {
927 /* XXX: this feels dubious - is it really what Bv2.1 means? */
928 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
929 if (distance >= 0 && distance < 2) {
933 last_out = reel_offset + out.as_editable_units_floor(er);
934 } else if (node->name() == "Text") {
935 std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
936 if (!node->content().empty()) {
939 for (auto i: node->node_children()) {
940 if (node_has_content(i)) {
946 if (!node_has_content(node)) {
950 } else if (node->name() == "LoadFont") {
951 if (auto const id = node->optional_string_attribute("Id")) {
952 font_ids.push_back(*id);
953 } else if (auto const id = node->optional_string_attribute("ID")) {
954 font_ids.push_back(*id);
956 } else if (node->name() == "Font") {
957 if (auto const font_id = node->optional_string_attribute("Id")) {
958 if (std::find_if(font_ids.begin(), font_ids.end(), [font_id](string const& id) { return id == font_id; }) == font_ids.end()) {
959 missing_load_font_id = font_id;
963 for (auto i: node->node_children()) {
964 parse(i, tcr, start_time, er, first_reel, has_text, font_ids);
968 for (auto i = 0U; i < reels.size(); ++i) {
969 if (!check(reels[i])) {
973 auto reel_xml = xml(reels[i]);
975 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
979 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
980 * read in by libdcp's parser.
983 shared_ptr<cxml::Document> doc;
985 optional<Time> start_time;
987 case dcp::Standard::INTEROP:
988 doc = make_shared<cxml::Document>("DCSubtitle");
989 doc->read_string (*reel_xml);
991 case dcp::Standard::SMPTE:
992 doc = make_shared<cxml::Document>("SubtitleReel");
993 doc->read_string (*reel_xml);
994 tcr = doc->number_child<int>("TimeCodeRate");
995 if (auto start_time_string = doc->optional_string_child("StartTime")) {
996 start_time = Time(*start_time_string, tcr);
1000 bool has_text = false;
1001 vector<string> font_ids;
1002 parse(doc, tcr, start_time, edit_rate, i == 0, has_text, font_ids);
1003 auto end = reel_offset + duration(reels[i]);
1004 if (last_out && *last_out > end) {
1005 reel_overlap = true;
1009 if (standard == dcp::Standard::SMPTE && has_text && font_ids.empty()) {
1010 notes.push_back(dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT).set_id(id(reels[i])));
1014 if (last_out && *last_out > reel_offset) {
1015 reel_overlap = true;
1020 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
1026 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
1032 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
1038 VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
1044 VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_TEXT
1048 if (missing_load_font_id) {
1049 notes.push_back(dcp::VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT).set_id(*missing_load_font_id));
1056 verify_closed_caption_details (
1057 vector<shared_ptr<Reel>> reels,
1058 vector<VerificationNote>& notes
1061 std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
1062 find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
1063 for (auto i: node->node_children()) {
1064 if (i->name() == "Text") {
1065 text_or_image.push_back (i);
1067 find_text_or_image (i, text_or_image);
1072 auto mismatched_valign = false;
1073 auto incorrect_order = false;
1075 std::function<void (cxml::ConstNodePtr)> parse;
1076 parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
1077 if (node->name() == "Subtitle") {
1078 vector<cxml::ConstNodePtr> text_or_image;
1079 find_text_or_image (node, text_or_image);
1080 optional<string> last_valign;
1081 optional<float> last_vpos;
1082 for (auto i: text_or_image) {
1083 auto valign = i->optional_string_attribute("VAlign");
1085 valign = i->optional_string_attribute("Valign").get_value_or("center");
1087 auto vpos = i->optional_number_attribute<float>("VPosition");
1089 vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
1093 if (*last_valign != valign) {
1094 mismatched_valign = true;
1097 last_valign = valign;
1099 if (!mismatched_valign) {
1101 if (*last_valign == "top" || *last_valign == "center") {
1102 if (*vpos < *last_vpos) {
1103 incorrect_order = true;
1106 if (*vpos > *last_vpos) {
1107 incorrect_order = true;
1116 for (auto i: node->node_children()) {
1121 for (auto reel: reels) {
1122 for (auto ccap: reel->closed_captions()) {
1123 auto reel_xml = ccap->asset()->raw_xml();
1125 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
1129 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1130 * read in by libdcp's parser.
1133 shared_ptr<cxml::Document> doc;
1135 optional<Time> start_time;
1137 doc = make_shared<cxml::Document>("SubtitleReel");
1138 doc->read_string (*reel_xml);
1140 doc = make_shared<cxml::Document>("DCSubtitle");
1141 doc->read_string (*reel_xml);
1147 if (mismatched_valign) {
1149 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN,
1153 if (incorrect_order) {
1155 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING,
1161 struct LinesCharactersResult
1163 bool warning_length_exceeded = false;
1164 bool error_length_exceeded = false;
1165 bool line_count_exceeded = false;
1171 verify_text_lines_and_characters (
1172 shared_ptr<SubtitleAsset> asset,
1175 LinesCharactersResult* result
1181 Event (Time time_, float position_, int characters_)
1183 , position (position_)
1184 , characters (characters_)
1187 Event (Time time_, shared_ptr<Event> start_)
1193 int position; //< position from 0 at top of screen to 100 at bottom
1195 shared_ptr<Event> start;
1198 vector<shared_ptr<Event>> events;
1200 auto position = [](shared_ptr<const SubtitleString> sub) {
1201 switch (sub->v_align()) {
1203 return lrintf(sub->v_position() * 100);
1204 case VAlign::CENTER:
1205 return lrintf((0.5f + sub->v_position()) * 100);
1206 case VAlign::BOTTOM:
1207 return lrintf((1.0f - sub->v_position()) * 100);
1213 for (auto j: asset->subtitles()) {
1214 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1216 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1217 events.push_back(in);
1218 events.push_back(make_shared<Event>(text->out(), in));
1222 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1223 return a->time < b->time;
1226 map<int, int> current;
1227 for (auto i: events) {
1228 if (current.size() > 3) {
1229 result->line_count_exceeded = true;
1231 for (auto j: current) {
1232 if (j.second > warning_length) {
1233 result->warning_length_exceeded = true;
1235 if (j.second > error_length) {
1236 result->error_length_exceeded = true;
1241 /* end of a subtitle */
1242 DCP_ASSERT (current.find(i->start->position) != current.end());
1243 if (current[i->start->position] == i->start->characters) {
1244 current.erase(i->start->position);
1246 current[i->start->position] -= i->start->characters;
1249 /* start of a subtitle */
1250 if (current.find(i->position) == current.end()) {
1251 current[i->position] = i->characters;
1253 current[i->position] += i->characters;
1262 verify_text_details(dcp::Standard standard, vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1264 if (reels.empty()) {
1268 if (reels[0]->main_subtitle()) {
1269 verify_text_details(standard, reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1270 [](shared_ptr<Reel> reel) {
1271 return static_cast<bool>(reel->main_subtitle());
1273 [](shared_ptr<Reel> reel) {
1274 return reel->main_subtitle()->asset()->raw_xml();
1276 [](shared_ptr<Reel> reel) {
1277 return reel->main_subtitle()->actual_duration();
1279 [](shared_ptr<Reel> reel) {
1280 return reel->main_subtitle()->id();
1285 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1286 verify_text_details(standard, reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1287 [i](shared_ptr<Reel> reel) {
1288 return i < reel->closed_captions().size();
1290 [i](shared_ptr<Reel> reel) {
1291 return reel->closed_captions()[i]->asset()->raw_xml();
1293 [i](shared_ptr<Reel> reel) {
1294 return reel->closed_captions()[i]->actual_duration();
1296 [i](shared_ptr<Reel> reel) {
1297 return reel->closed_captions()[i]->id();
1302 verify_closed_caption_details (reels, notes);
1307 verify_extension_metadata(shared_ptr<const CPL> cpl, vector<VerificationNote>& notes)
1309 DCP_ASSERT (cpl->file());
1310 cxml::Document doc ("CompositionPlaylist");
1311 doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1313 auto missing = false;
1316 if (auto reel_list = doc.node_child("ReelList")) {
1317 auto reels = reel_list->node_children("Reel");
1318 if (!reels.empty()) {
1319 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1320 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1321 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1323 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1324 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1328 if (auto name = extension->optional_node_child("Name")) {
1329 if (name->content() != "Application") {
1330 malformed = "<Name> should be 'Application'";
1333 if (auto property_list = extension->optional_node_child("PropertyList")) {
1334 if (auto property = property_list->optional_node_child("Property")) {
1335 if (auto name = property->optional_node_child("Name")) {
1336 if (name->content() != "DCP Constraints Profile") {
1337 malformed = "<Name> property should be 'DCP Constraints Profile'";
1340 if (auto value = property->optional_node_child("Value")) {
1341 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1342 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1357 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1358 } else if (!malformed.empty()) {
1359 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1365 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1367 vector<string> encrypted;
1368 for (auto i: dcp->cpls()) {
1369 for (auto j: i->reel_file_assets()) {
1370 if (j->asset_ref().resolved()) {
1371 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1372 if (mxf && mxf->encrypted()) {
1373 encrypted.push_back(j->asset_ref().id());
1379 for (auto i: pkl->assets()) {
1380 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1392 shared_ptr<const DCP> dcp,
1393 shared_ptr<const CPL> cpl,
1394 shared_ptr<const Reel> reel,
1395 int64_t start_frame,
1396 optional<dcp::Size> main_picture_active_area,
1397 function<void (string, optional<boost::filesystem::path>)> stage,
1398 boost::filesystem::path xsd_dtd_directory,
1399 function<void (float)> progress,
1400 VerificationOptions options,
1401 vector<VerificationNote>& notes,
1403 bool* have_main_subtitle,
1404 bool* have_no_main_subtitle,
1405 size_t* most_closed_captions,
1406 size_t* fewest_closed_captions,
1407 map<Marker, Time>* markers_seen
1410 for (auto i: reel->assets()) {
1411 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1412 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1414 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1415 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1417 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1418 if (i->encryptable() && !file_asset->hash()) {
1419 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1423 if (dcp->standard() == Standard::SMPTE) {
1424 boost::optional<int64_t> duration;
1425 for (auto i: reel->assets()) {
1427 duration = i->actual_duration();
1428 } else if (*duration != i->actual_duration()) {
1429 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1435 if (reel->main_picture()) {
1436 /* Check reel stuff */
1437 auto const frame_rate = reel->main_picture()->frame_rate();
1438 if (frame_rate.denominator != 1 ||
1439 (frame_rate.numerator != 24 &&
1440 frame_rate.numerator != 25 &&
1441 frame_rate.numerator != 30 &&
1442 frame_rate.numerator != 48 &&
1443 frame_rate.numerator != 50 &&
1444 frame_rate.numerator != 60 &&
1445 frame_rate.numerator != 96)) {
1447 VerificationNote::Type::ERROR,
1448 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1449 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1453 if (reel->main_picture()->asset_ref().resolved()) {
1454 verify_main_picture_asset(dcp, reel->main_picture(), start_frame, stage, progress, options, notes);
1455 auto const asset_size = reel->main_picture()->asset()->size();
1456 if (main_picture_active_area) {
1457 if (main_picture_active_area->width > asset_size.width) {
1459 VerificationNote::Type::ERROR,
1460 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1461 String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1465 if (main_picture_active_area->height > asset_size.height) {
1467 VerificationNote::Type::ERROR,
1468 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1469 String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1478 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1479 verify_main_sound_asset(dcp, reel->main_sound(), stage, progress, options, notes, state);
1482 if (reel->main_subtitle()) {
1483 verify_main_subtitle_reel(reel->main_subtitle(), notes);
1484 if (reel->main_subtitle()->asset_ref().resolved()) {
1485 verify_subtitle_asset(reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, xsd_dtd_directory, notes, state);
1487 *have_main_subtitle = true;
1489 *have_no_main_subtitle = true;
1492 for (auto i: reel->closed_captions()) {
1493 verify_closed_caption_reel(i, notes);
1494 if (i->asset_ref().resolved()) {
1495 verify_closed_caption_asset(i->asset(), i->duration(), stage, xsd_dtd_directory, notes);
1499 if (reel->main_markers()) {
1500 for (auto const& i: reel->main_markers()->get()) {
1501 markers_seen->insert(i);
1503 if (reel->main_markers()->entry_point()) {
1504 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_ENTRY_POINT});
1506 if (reel->main_markers()->duration()) {
1507 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_DURATION});
1511 *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1512 *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1520 shared_ptr<const DCP> dcp,
1521 shared_ptr<const CPL> cpl,
1522 function<void (string, optional<boost::filesystem::path>)> stage,
1523 boost::filesystem::path xsd_dtd_directory,
1524 function<void (float)> progress,
1525 VerificationOptions options,
1526 vector<VerificationNote>& notes,
1530 stage("Checking CPL", cpl->file());
1531 validate_xml(cpl->file().get(), xsd_dtd_directory, notes);
1533 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1534 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1537 for (auto const& i: cpl->additional_subtitle_languages()) {
1538 verify_language_tag(i, notes);
1541 if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1542 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1543 * of the approved ones.
1545 auto all = ContentKind::all();
1546 auto name = cpl->content_kind().name();
1547 transform(name.begin(), name.end(), name.begin(), ::tolower);
1548 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1549 if (iter == all.end()) {
1550 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name()});
1554 if (cpl->release_territory()) {
1555 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") {
1556 auto terr = cpl->release_territory().get();
1557 /* Must be a valid region tag, or "001" */
1559 LanguageTag::RegionSubtag test(terr);
1561 if (terr != "001") {
1562 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1568 for (auto version: cpl->content_versions()) {
1569 if (version.label_text.empty()) {
1571 dcp::VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT, cpl->file().get()).set_id(cpl->id())
1577 if (dcp->standard() == Standard::SMPTE) {
1578 if (!cpl->annotation_text()) {
1579 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1580 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1581 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1585 for (auto i: dcp->pkls()) {
1586 /* Check that the CPL's hash corresponds to the PKL */
1587 optional<string> h = i->hash(cpl->id());
1588 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1589 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1592 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1593 optional<string> required_annotation_text;
1594 for (auto j: i->assets()) {
1595 /* See if this is a CPL */
1596 for (auto k: dcp->cpls()) {
1597 if (j->id() == k->id()) {
1598 if (!required_annotation_text) {
1599 /* First CPL we have found; this is the required AnnotationText unless we find another */
1600 required_annotation_text = cpl->content_title_text();
1602 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1603 required_annotation_text = boost::none;
1609 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1610 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1614 /* set to true if any reel has a MainSubtitle */
1615 auto have_main_subtitle = false;
1616 /* set to true if any reel has no MainSubtitle */
1617 auto have_no_main_subtitle = false;
1618 /* fewest number of closed caption assets seen in a reel */
1619 size_t fewest_closed_captions = SIZE_MAX;
1620 /* most number of closed caption assets seen in a reel */
1621 size_t most_closed_captions = 0;
1622 map<Marker, Time> markers_seen;
1624 auto const main_picture_active_area = cpl->main_picture_active_area();
1625 if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1627 VerificationNote::Type::ERROR,
1628 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1629 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1633 if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1635 VerificationNote::Type::ERROR,
1636 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1637 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1643 for (auto reel: cpl->reels()) {
1644 stage("Checking reel", optional<boost::filesystem::path>());
1650 main_picture_active_area,
1657 &have_main_subtitle,
1658 &have_no_main_subtitle,
1659 &most_closed_captions,
1660 &fewest_closed_captions,
1663 frame += reel->duration();
1666 verify_text_details(dcp->standard().get_value_or(dcp::Standard::SMPTE), cpl->reels(), notes);
1668 if (dcp->standard() == Standard::SMPTE) {
1669 if (auto msc = cpl->main_sound_configuration()) {
1670 if (state.audio_channels && msc->channels() != *state.audio_channels) {
1672 VerificationNote::Type::ERROR,
1673 VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION,
1674 String::compose("MainSoundConfiguration has %1 channels but sound assets have %2", msc->channels(), *state.audio_channels),
1680 if (have_main_subtitle && have_no_main_subtitle) {
1681 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1684 if (fewest_closed_captions != most_closed_captions) {
1685 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1688 if (cpl->content_kind() == ContentKind::FEATURE) {
1689 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1690 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1692 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1693 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1697 auto ffoc = markers_seen.find(Marker::FFOC);
1698 if (ffoc == markers_seen.end()) {
1699 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1700 } else if (ffoc->second.e != 1) {
1701 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1704 auto lfoc = markers_seen.find(Marker::LFOC);
1705 if (lfoc == markers_seen.end()) {
1706 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1708 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1709 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1710 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1714 LinesCharactersResult result;
1715 for (auto reel: cpl->reels()) {
1716 if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1717 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1721 if (result.line_count_exceeded) {
1722 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1724 if (result.error_length_exceeded) {
1725 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1726 } else if (result.warning_length_exceeded) {
1727 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1730 result = LinesCharactersResult();
1731 for (auto reel: cpl->reels()) {
1732 for (auto i: reel->closed_captions()) {
1734 verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1739 if (result.line_count_exceeded) {
1740 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1742 if (result.error_length_exceeded) {
1743 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1746 if (!cpl->read_composition_metadata()) {
1747 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1748 } else if (!cpl->version_number()) {
1749 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1752 verify_extension_metadata(cpl, notes);
1754 if (cpl->any_encrypted()) {
1755 cxml::Document doc("CompositionPlaylist");
1756 DCP_ASSERT(cpl->file());
1757 doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1758 if (!doc.optional_node_child("Signature")) {
1759 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1769 shared_ptr<const DCP> dcp,
1770 shared_ptr<const PKL> pkl,
1771 boost::filesystem::path xsd_dtd_directory,
1772 vector<VerificationNote>& notes
1775 validate_xml(pkl->file().get(), xsd_dtd_directory, notes);
1777 if (pkl_has_encrypted_assets(dcp, pkl)) {
1778 cxml::Document doc("PackingList");
1779 doc.read_file(dcp::filesystem::fix_long_path(pkl->file().get()));
1780 if (!doc.optional_node_child("Signature")) {
1781 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1785 set<string> uuid_set;
1786 for (auto asset: pkl->assets()) {
1787 if (!uuid_set.insert(asset->id()).second) {
1788 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get()});
1799 shared_ptr<const DCP> dcp,
1800 boost::filesystem::path xsd_dtd_directory,
1801 vector<VerificationNote>& notes
1804 auto asset_map = dcp->asset_map();
1805 DCP_ASSERT(asset_map);
1807 validate_xml(asset_map->file().get(), xsd_dtd_directory, notes);
1809 set<string> uuid_set;
1810 for (auto const& asset: asset_map->assets()) {
1811 if (!uuid_set.insert(asset.id()).second) {
1812 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get()});
1819 vector<VerificationNote>
1821 vector<boost::filesystem::path> directories,
1822 vector<dcp::DecryptedKDM> kdms,
1823 function<void (string, optional<boost::filesystem::path>)> stage,
1824 function<void (float)> progress,
1825 VerificationOptions options,
1826 optional<boost::filesystem::path> xsd_dtd_directory
1829 if (!xsd_dtd_directory) {
1830 xsd_dtd_directory = resources_directory() / "xsd";
1832 *xsd_dtd_directory = filesystem::canonical(*xsd_dtd_directory);
1834 vector<VerificationNote> notes;
1837 vector<shared_ptr<DCP>> dcps;
1838 for (auto i: directories) {
1839 dcps.push_back (make_shared<DCP>(i));
1842 for (auto dcp: dcps) {
1843 stage ("Checking DCP", dcp->directory());
1844 bool carry_on = true;
1846 dcp->read (¬es, true);
1847 } catch (MissingAssetmapError& e) {
1848 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1850 } catch (ReadError& e) {
1851 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1852 } catch (XMLError& e) {
1853 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1854 } catch (MXFFileError& e) {
1855 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1856 } catch (BadURNUUIDError& e) {
1857 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1858 } catch (cxml::Error& e) {
1859 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1866 if (dcp->standard() != Standard::SMPTE) {
1867 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1870 for (auto kdm: kdms) {
1874 for (auto cpl: dcp->cpls()) {
1887 for (auto pkl: dcp->pkls()) {
1888 stage("Checking PKL", pkl->file());
1889 verify_pkl(dcp, pkl, *xsd_dtd_directory, notes);
1892 if (dcp->asset_map_file()) {
1893 stage("Checking ASSETMAP", dcp->asset_map_file().get());
1894 verify_assetmap(dcp, *xsd_dtd_directory, notes);
1896 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1905 dcp::note_to_string (VerificationNote note)
1907 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1909 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1910 * not "ClosedCaption assets must have an <EntryPoint> tag."
1912 * It's OK to use XML tag names where they are clear.
1913 * If both ID and filename are available, use only the ID.
1914 * End messages with a full stop.
1915 * Messages should not mention whether or not their errors are a part of Bv2.1.
1917 switch (note.code()) {
1918 case VerificationNote::Code::FAILED_READ:
1919 return *note.note();
1920 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1921 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1922 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1923 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1924 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1925 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1926 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1927 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1928 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1929 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1930 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1931 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1932 case VerificationNote::Code::EMPTY_ASSET_PATH:
1933 return "The asset map contains an empty asset path.";
1934 case VerificationNote::Code::MISSING_ASSET:
1935 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1936 case VerificationNote::Code::MISMATCHED_STANDARD:
1937 return "The DCP contains both SMPTE and Interop parts.";
1938 case VerificationNote::Code::INVALID_XML:
1939 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1940 case VerificationNote::Code::MISSING_ASSETMAP:
1941 return "No valid ASSETMAP or ASSETMAP.xml was found.";
1942 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1943 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1944 case VerificationNote::Code::INVALID_DURATION:
1945 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1946 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1947 return String::compose(
1948 "Frame %1 (timecode %2) in asset %3 has an instantaneous bit rate that is larger than the limit of 250Mbit/s.",
1950 dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
1951 note.file()->filename()
1953 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1954 return String::compose(
1955 "Frame %1 (timecode %2) in asset %3 has an instantaneous bit rate that is close to the limit of 250Mbit/s.",
1957 dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
1958 note.file()->filename()
1960 case VerificationNote::Code::EXTERNAL_ASSET:
1961 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());
1962 case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1963 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1964 case VerificationNote::Code::INVALID_STANDARD:
1965 return "This DCP does not use the SMPTE standard.";
1966 case VerificationNote::Code::INVALID_LANGUAGE:
1967 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1968 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1969 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1970 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1971 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1972 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1973 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1974 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1975 return "3D 4K DCPs are not allowed.";
1976 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1977 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1978 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1979 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1980 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1981 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());
1982 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1983 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1984 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1985 return "Some subtitle assets have different <Language> tags than others";
1986 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1987 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1988 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1989 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1990 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1991 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1992 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1993 return "At least one subtitle lasts less than 15 frames.";
1994 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1995 return "At least one pair of subtitles is separated by less than 2 frames.";
1996 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1997 return "At least one subtitle extends outside of its reel.";
1998 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1999 return "There are more than 3 subtitle lines in at least one place in the DCP.";
2000 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
2001 return "There are more than 52 characters in at least one subtitle line.";
2002 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
2003 return "There are more than 79 characters in at least one subtitle line.";
2004 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
2005 return "There are more than 3 closed caption lines in at least one place.";
2006 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
2007 return "There are more than 32 characters in at least one closed caption line.";
2008 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
2009 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
2010 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
2011 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
2012 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
2013 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.note().get());
2014 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
2015 return "All assets in a reel do not have the same duration.";
2016 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
2017 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
2018 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
2019 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
2020 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
2021 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
2022 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
2023 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
2024 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
2025 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
2026 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
2027 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
2028 case VerificationNote::Code::MISSING_HASH:
2029 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
2030 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
2031 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
2032 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
2033 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
2034 case VerificationNote::Code::MISSING_FFOC:
2035 return "There should be a FFOC (first frame of content) marker.";
2036 case VerificationNote::Code::MISSING_LFOC:
2037 return "There should be a LFOC (last frame of content) marker.";
2038 case VerificationNote::Code::INCORRECT_FFOC:
2039 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
2040 case VerificationNote::Code::INCORRECT_LFOC:
2041 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
2042 case VerificationNote::Code::MISSING_CPL_METADATA:
2043 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
2044 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
2045 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
2046 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
2047 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
2048 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
2049 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
2050 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
2051 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
2052 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
2053 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
2054 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
2055 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
2056 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
2057 return "Some assets are encrypted but some are not.";
2058 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
2059 return String::compose(
2060 "Frame %1 (timecode %2) has an invalid JPEG2000 codestream (%2).",
2062 dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
2065 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
2066 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
2067 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
2068 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
2069 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
2070 return "The JPEG2000 tile size is not the same as the image size.";
2071 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
2072 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
2073 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
2074 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
2075 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
2076 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
2077 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
2078 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
2079 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
2080 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
2081 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
2082 return "POC marker found outside main header.";
2083 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
2084 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
2085 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
2086 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
2087 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
2088 return "No TLM marker was found in a JPEG2000 codestream.";
2089 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
2090 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
2091 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
2092 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
2093 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
2095 vector<string> parts;
2096 boost::split (parts, note.note().get(), boost::is_any_of(" "));
2097 DCP_ASSERT (parts.size() == 2);
2098 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]);
2100 case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
2101 return "Some aspect of this DCP could not be checked because it is encrypted.";
2102 case VerificationNote::Code::EMPTY_TEXT:
2103 return "There is an empty <Text> node in a subtitle or closed caption.";
2104 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
2105 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
2106 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
2107 return "Some closed captions are not listed in the order of their vertical position.";
2108 case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
2109 return "There is an <EntryPoint> node inside a <MainMarkers>.";
2110 case VerificationNote::Code::UNEXPECTED_DURATION:
2111 return "There is an <Duration> node inside a <MainMarkers>.";
2112 case VerificationNote::Code::INVALID_CONTENT_KIND:
2113 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
2114 case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
2115 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
2116 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
2117 return String::compose("The PKL %1 has more than one asset with the same ID.", note.note().get());
2118 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
2119 return String::compose("The ASSETMAP %1 has more than one asset with the same ID.", note.note().get());
2120 case VerificationNote::Code::MISSING_SUBTITLE:
2121 return String::compose("The subtitle asset %1 has no subtitles.", note.note().get());
2122 case VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
2123 return String::compose("<IssueDate> has an invalid value: %1", note.note().get());
2124 case VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS:
2125 return String::compose("The sound assets do not all have the same channel count; the first to differ is %1", note.file()->filename());
2126 case VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION:
2127 return String::compose("<MainSoundConfiguration> has an invalid value: %1", note.note().get());
2128 case VerificationNote::Code::MISSING_FONT:
2129 return String::compose("The font file for font ID \"%1\" was not found, or was not referred to in the ASSETMAP.", note.note().get());
2130 case VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE:
2131 return String::compose(
2132 "Frame %1 has an image component that is too large (component %2 is %3 bytes in size).",
2133 note.frame().get(), note.component().get(), note.size().get()
2135 case VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT:
2136 return String::compose("The XML in the subtitle asset %1 has more than one namespace declaration.", note.note().get());
2137 case VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT:
2138 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());
2139 case VerificationNote::Code::MISSING_LOAD_FONT:
2140 return String::compose("The SMPTE subtitle asset %1 has <Text> nodes but no <LoadFont> node", note.id().get());
2141 case VerificationNote::Code::MISMATCHED_ASSET_MAP_ID:
2142 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());
2143 case VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT:
2144 return String::compose("The <LabelText> in a <ContentVersion> in CPL %1 is empty", note.id().get());
2152 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2154 return a.type() == b.type() &&
2155 a.code() == b.code() &&
2156 a.note() == b.note() &&
2157 a.file() == b.file() &&
2158 a.line() == b.line() &&
2159 a.frame() == b.frame() &&
2160 a.component() == b.component() &&
2161 a.size() == b.size() &&
2163 a.other_id() == b.other_id() &&
2164 a.frame_rate() == b.frame_rate();
2169 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2171 if (a.type() != b.type()) {
2172 return a.type() < b.type();
2175 if (a.code() != b.code()) {
2176 return a.code() < b.code();
2179 if (a.note() != b.note()) {
2180 return a.note().get_value_or("") < b.note().get_value_or("");
2183 if (a.file() != b.file()) {
2184 return a.file().get_value_or("") < b.file().get_value_or("");
2187 if (a.line() != b.line()) {
2188 return a.line().get_value_or(0) < b.line().get_value_or(0);
2191 if (a.frame() != b.frame()) {
2192 return a.frame().get_value_or(0) < b.frame().get_value_or(0);
2195 if (a.component() != b.component()) {
2196 return a.component().get_value_or(0) < b.component().get_value_or(0);
2199 if (a.size() != b.size()) {
2200 return a.size().get_value_or(0) < b.size().get_value_or(0);
2203 if (a.id() != b.id()) {
2204 return a.id().get_value_or("") < b.id().get_value_or("");
2207 if (a.other_id() != b.other_id()) {
2208 return a.other_id().get_value_or("") < b.other_id().get_value_or("");
2211 return a.frame_rate().get_value_or(0) != b.frame_rate().get_value_or(0);
2216 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2218 s << note_to_string (note);
2220 s << " [" << note.note().get() << "]";
2223 s << " [" << note.file().get() << "]";
2226 s << " [" << note.line().get() << "]";