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;
93 using std::make_shared;
97 using std::shared_ptr;
100 using boost::optional;
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(), "");
300 std::vector<VerificationNote>& notes_,
301 boost::filesystem::path xsd_dtd_directory_,
302 function<void (string, optional<boost::filesystem::path>)> stage_,
303 function<void (float)> progress_,
304 VerificationOptions options_
307 , xsd_dtd_directory(xsd_dtd_directory_)
309 , progress(progress_)
315 Context(Context const&) = delete;
316 Context& operator=(Context const&) = delete;
318 template<typename... Args>
319 void ok(dcp::VerificationNote::Code code, Args... args)
321 add_note({dcp::VerificationNote::Type::OK, code, std::forward<Args>(args)...});
324 template<typename... Args>
325 void warning(dcp::VerificationNote::Code code, Args... args)
327 add_note({dcp::VerificationNote::Type::WARNING, code, std::forward<Args>(args)...});
330 template<typename... Args>
331 void bv21_error(dcp::VerificationNote::Code code, Args... args)
333 add_note({dcp::VerificationNote::Type::BV21_ERROR, code, std::forward<Args>(args)...});
336 template<typename... Args>
337 void error(dcp::VerificationNote::Code code, Args... args)
339 add_note({dcp::VerificationNote::Type::ERROR, code, std::forward<Args>(args)...});
342 void add_note(dcp::VerificationNote note)
345 note.set_cpl_id(cpl->id());
347 notes.push_back(std::move(note));
350 void add_note_if_not_existing(dcp::VerificationNote note)
352 if (find(notes.begin(), notes.end(), note) == notes.end()) {
357 std::vector<VerificationNote>& notes;
358 std::shared_ptr<const DCP> dcp;
359 std::shared_ptr<const CPL> cpl;
360 boost::filesystem::path xsd_dtd_directory;
361 function<void (string, optional<boost::filesystem::path>)> stage;
362 function<void (float)> progress;
363 VerificationOptions options;
365 boost::optional<string> subtitle_language;
366 boost::optional<int> audio_channels;
372 validate_xml(Context& context, T xml)
375 XMLPlatformUtils::Initialize ();
376 } catch (XMLException& e) {
377 throw MiscError ("Failed to initialise xerces library");
380 DCPErrorHandler error_handler;
382 /* All the xerces objects in this scope must be destroyed before XMLPlatformUtils::Terminate() is called */
384 XercesDOMParser parser;
385 parser.setValidationScheme(XercesDOMParser::Val_Always);
386 parser.setDoNamespaces(true);
387 parser.setDoSchema(true);
389 vector<string> schema;
390 schema.push_back("xml.xsd");
391 schema.push_back("xmldsig-core-schema.xsd");
392 schema.push_back("SMPTE-429-7-2006-CPL.xsd");
393 schema.push_back("SMPTE-429-8-2006-PKL.xsd");
394 schema.push_back("SMPTE-429-9-2007-AM.xsd");
395 schema.push_back("Main-Stereo-Picture-CPL.xsd");
396 schema.push_back("PROTO-ASDCP-CPL-20040511.xsd");
397 schema.push_back("PROTO-ASDCP-PKL-20040311.xsd");
398 schema.push_back("PROTO-ASDCP-AM-20040311.xsd");
399 schema.push_back("DCSubtitle.v1.mattsson.xsd");
400 schema.push_back("DCDMSubtitle-2010.xsd");
401 schema.push_back("DCDMSubtitle-2014.xsd");
402 schema.push_back("PROTO-ASDCP-CC-CPL-20070926.xsd");
403 schema.push_back("SMPTE-429-16.xsd");
404 schema.push_back("Dolby-2012-AD.xsd");
405 schema.push_back("SMPTE-429-10-2008.xsd");
406 schema.push_back("xlink.xsd");
407 schema.push_back("SMPTE-335-2012.xsd");
408 schema.push_back("SMPTE-395-2014-13-1-aaf.xsd");
409 schema.push_back("isdcf-mca.xsd");
410 schema.push_back("SMPTE-429-12-2008.xsd");
412 /* XXX: I'm not especially clear what this is for, but it seems to be necessary.
413 * Schemas that are not mentioned in this list are not read, and the things
414 * they describe are not checked.
417 for (auto i: schema) {
418 locations += String::compose("%1 %1 ", i, i);
421 parser.setExternalSchemaLocation(locations.c_str());
422 parser.setValidationSchemaFullChecking(true);
423 parser.setErrorHandler(&error_handler);
425 LocalFileResolver resolver(context.xsd_dtd_directory);
426 parser.setEntityResolver(&resolver);
429 parser.resetDocumentPool();
431 } catch (XMLException& e) {
432 throw MiscError(xml_ch_to_string(e.getMessage()));
433 } catch (DOMException& e) {
434 throw MiscError(xml_ch_to_string(e.getMessage()));
436 throw MiscError("Unknown exception from xerces");
440 XMLPlatformUtils::Terminate ();
442 for (auto i: error_handler.errors()) {
444 VerificationNote::Code::INVALID_XML,
446 boost::trim_copy(i.public_id() + " " + i.system_id()),
453 enum class VerifyAssetResult {
460 static VerifyAssetResult
463 shared_ptr<const ReelFileAsset> reel_file_asset,
464 string* reference_hash,
465 string* calculated_hash
468 DCP_ASSERT(reference_hash);
469 DCP_ASSERT(calculated_hash);
471 /* When reading the DCP the hash will have been set to the one from the PKL/CPL.
472 * We want to calculate the hash of the actual file contents here, so that we
473 * can check it. unset_hash() means that this calculation will happen on the
476 reel_file_asset->asset_ref()->unset_hash();
477 *calculated_hash = reel_file_asset->asset_ref()->hash([&context](int64_t done, int64_t total) {
478 context.progress(float(done) / total);
481 auto pkls = context.dcp->pkls();
482 /* We've read this DCP in so it must have at least one PKL */
483 DCP_ASSERT (!pkls.empty());
485 auto asset = reel_file_asset->asset_ref().asset();
487 optional<string> maybe_pkl_hash;
489 maybe_pkl_hash = i->hash (reel_file_asset->asset_ref()->id());
490 if (maybe_pkl_hash) {
495 DCP_ASSERT(maybe_pkl_hash);
496 *reference_hash = *maybe_pkl_hash;
498 auto cpl_hash = reel_file_asset->hash();
499 if (cpl_hash && *cpl_hash != *reference_hash) {
500 return VerifyAssetResult::CPL_PKL_DIFFER;
503 if (*calculated_hash != *reference_hash) {
504 return VerifyAssetResult::BAD;
507 return VerifyAssetResult::GOOD;
512 verify_language_tag(Context& context, string tag)
515 LanguageTag test (tag);
516 } catch (LanguageTagError &) {
517 context.bv21_error(VerificationNote::Code::INVALID_LANGUAGE, tag);
523 verify_picture_asset(
525 shared_ptr<const ReelFileAsset> reel_file_asset,
526 boost::filesystem::path file,
530 auto asset = dynamic_pointer_cast<PictureAsset>(reel_file_asset->asset_ref().asset());
531 auto const duration = asset->intrinsic_duration ();
533 auto check_and_add = [&context](vector<VerificationNote> const& j2k_notes) {
534 for (auto i: j2k_notes) {
535 context.add_note_if_not_existing(i);
539 int const max_frame = rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
540 int const risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
542 auto check_frame_size = [max_frame, risky_frame, file, start_frame](Context& context, int index, int size, int frame_rate) {
543 if (size > max_frame) {
546 VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
547 ).set_frame(start_frame + index).set_frame_rate(frame_rate)
549 } else if (size > risky_frame) {
552 VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
553 ).set_frame(start_frame + index).set_frame_rate(frame_rate)
558 if (auto mono_asset = dynamic_pointer_cast<MonoPictureAsset>(reel_file_asset->asset_ref().asset())) {
559 auto reader = mono_asset->start_read ();
560 for (int64_t i = 0; i < duration; ++i) {
561 auto frame = reader->get_frame (i);
562 check_frame_size(context, i, frame->size(), mono_asset->frame_rate().numerator);
563 if (!mono_asset->encrypted() || mono_asset->key()) {
564 vector<VerificationNote> j2k_notes;
565 verify_j2k(frame, start_frame, i, mono_asset->frame_rate().numerator, j2k_notes);
566 check_and_add (j2k_notes);
568 context.progress(float(i) / duration);
570 } else if (auto stereo_asset = dynamic_pointer_cast<StereoPictureAsset>(asset)) {
571 auto reader = stereo_asset->start_read ();
572 for (int64_t i = 0; i < duration; ++i) {
573 auto frame = reader->get_frame (i);
574 check_frame_size(context, i, frame->left()->size(), stereo_asset->frame_rate().numerator);
575 check_frame_size(context, i, frame->right()->size(), stereo_asset->frame_rate().numerator);
576 if (!stereo_asset->encrypted() || stereo_asset->key()) {
577 vector<VerificationNote> j2k_notes;
578 verify_j2k(frame->left(), start_frame, i, stereo_asset->frame_rate().numerator, j2k_notes);
579 verify_j2k(frame->right(), start_frame, i, stereo_asset->frame_rate().numerator, j2k_notes);
580 check_and_add (j2k_notes);
582 context.progress(float(i) / duration);
590 verify_main_picture_asset(Context& context, shared_ptr<const ReelPictureAsset> reel_asset, int64_t start_frame)
592 auto asset = reel_asset->asset();
593 auto const file = *asset->file();
595 if (context.options.check_asset_hashes && (!context.options.maximum_asset_size_for_hash_check || filesystem::file_size(file) < *context.options.maximum_asset_size_for_hash_check)) {
596 context.stage("Checking picture asset hash", file);
597 string reference_hash;
598 string calculated_hash;
599 auto const r = verify_asset(context, reel_asset, &reference_hash, &calculated_hash);
601 case VerifyAssetResult::BAD:
603 dcp::VerificationNote(
604 VerificationNote::Type::ERROR,
605 VerificationNote::Code::INCORRECT_PICTURE_HASH,
607 ).set_reference_hash(reference_hash).set_calculated_hash(calculated_hash)
610 case VerifyAssetResult::CPL_PKL_DIFFER:
611 context.error(VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file);
618 context.stage("Checking picture frame sizes", asset->file());
619 verify_picture_asset(context, reel_asset, file, start_frame);
621 /* Only flat/scope allowed by Bv2.1 */
623 asset->size() != Size(2048, 858) &&
624 asset->size() != Size(1998, 1080) &&
625 asset->size() != Size(4096, 1716) &&
626 asset->size() != Size(3996, 2160)) {
627 context.bv21_error(VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS, String::compose("%1x%2", asset->size().width, asset->size().height), file);
630 /* Only 24, 25, 48fps allowed for 2K */
632 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
633 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
636 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
637 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
642 if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
643 /* Only 24fps allowed for 4K */
644 if (asset->edit_rate() != Fraction(24, 1)) {
646 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
647 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
652 /* Only 2D allowed for 4K */
653 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
655 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
656 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
666 verify_main_sound_asset(Context& context, shared_ptr<const ReelSoundAsset> reel_asset)
668 auto asset = reel_asset->asset();
669 auto const file = *asset->file();
671 if (context.options.check_asset_hashes && (!context.options.maximum_asset_size_for_hash_check || filesystem::file_size(file) < *context.options.maximum_asset_size_for_hash_check)) {
672 context.stage("Checking sound asset hash", file);
673 string reference_hash;
674 string calculated_hash;
675 auto const r = verify_asset(context, reel_asset, &reference_hash, &calculated_hash);
677 case VerifyAssetResult::BAD:
679 dcp::VerificationNote(
680 VerificationNote::Type::ERROR,
681 VerificationNote::Code::INCORRECT_SOUND_HASH,
683 ).set_reference_hash(reference_hash).set_calculated_hash(calculated_hash)
686 case VerifyAssetResult::CPL_PKL_DIFFER:
687 context.error(VerificationNote::Code::MISMATCHED_SOUND_HASHES, file);
694 if (!context.audio_channels) {
695 context.audio_channels = asset->channels();
696 } else if (*context.audio_channels != asset->channels()) {
697 context.error(VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS, file);
700 context.stage("Checking sound asset metadata", file);
702 if (auto lang = asset->language()) {
703 verify_language_tag(context, *lang);
705 if (asset->sampling_rate() != 48000) {
706 context.bv21_error(VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), file);
712 verify_main_subtitle_reel(Context& context, shared_ptr<const ReelSubtitleAsset> reel_asset)
714 /* XXX: is Language compulsory? */
715 if (reel_asset->language()) {
716 verify_language_tag(context, *reel_asset->language());
719 if (!reel_asset->entry_point()) {
720 context.bv21_error(VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id());
721 } else if (reel_asset->entry_point().get()) {
722 context.bv21_error(VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id());
728 verify_closed_caption_reel(Context& context, shared_ptr<const ReelClosedCaptionAsset> reel_asset)
730 /* XXX: is Language compulsory? */
731 if (reel_asset->language()) {
732 verify_language_tag(context, *reel_asset->language());
735 if (!reel_asset->entry_point()) {
736 context.bv21_error(VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id());
737 } else if (reel_asset->entry_point().get()) {
738 context.bv21_error(VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id());
743 /** Verify stuff that is common to both subtitles and closed captions */
745 verify_smpte_timed_text_asset (
747 shared_ptr<const SMPTESubtitleAsset> asset,
748 optional<int64_t> reel_asset_duration
751 if (asset->language()) {
752 verify_language_tag(context, *asset->language());
754 context.bv21_error(VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file());
757 auto const size = filesystem::file_size(asset->file().get());
758 if (size > 115 * 1024 * 1024) {
759 context.bv21_error(VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file());
762 /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
763 * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
765 auto fonts = asset->font_data ();
767 for (auto i: fonts) {
768 total_size += i.second.size();
770 if (total_size > 10 * 1024 * 1024) {
771 context.bv21_error(VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get());
774 if (!asset->start_time()) {
775 context.bv21_error(VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get());
776 } else if (asset->start_time() != Time()) {
777 context.bv21_error(VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get());
780 if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
782 VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
783 String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
790 /** Verify Interop subtitle / CCAP stuff */
792 verify_interop_text_asset(Context& context, shared_ptr<const InteropSubtitleAsset> asset)
794 if (asset->subtitles().empty()) {
795 context.error(VerificationNote::Code::MISSING_SUBTITLE, asset->id(), asset->file().get());
797 auto const unresolved = asset->unresolved_fonts();
798 if (!unresolved.empty()) {
799 context.error(VerificationNote::Code::MISSING_FONT, unresolved.front());
804 /** Verify SMPTE subtitle-only stuff */
806 verify_smpte_subtitle_asset(Context& context, shared_ptr<const SMPTESubtitleAsset> asset)
808 if (asset->language()) {
809 if (!context.subtitle_language) {
810 context.subtitle_language = *asset->language();
811 } else if (context.subtitle_language != *asset->language()) {
812 context.bv21_error(VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES);
816 DCP_ASSERT (asset->resource_id());
817 auto xml_id = asset->xml_id();
819 if (asset->resource_id().get() != xml_id) {
820 context.bv21_error(VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID);
823 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
824 context.bv21_error(VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID);
827 context.warning(VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED);
830 if (asset->raw_xml()) {
831 /* Deluxe require this in their QC even if it seems never to be mentioned in any standard */
832 cxml::Document doc("SubtitleReel");
833 doc.read_string(*asset->raw_xml());
834 auto issue_date = doc.string_child("IssueDate");
835 std::regex reg("^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d$");
836 if (!std::regex_match(issue_date, reg)) {
837 context.warning(VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, issue_date);
843 /** Verify all subtitle stuff */
845 verify_subtitle_asset(Context& context, shared_ptr<const SubtitleAsset> asset, optional<int64_t> reel_asset_duration)
847 context.stage("Checking subtitle XML", asset->file());
848 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
849 * gets passed through libdcp which may clean up and therefore hide errors.
851 if (asset->raw_xml()) {
852 validate_xml(context, asset->raw_xml().get());
854 context.warning(VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED);
857 auto namespace_count = [](shared_ptr<const SubtitleAsset> asset, string root_node) {
858 cxml::Document doc(root_node);
859 doc.read_string(asset->raw_xml().get());
860 auto root = dynamic_cast<xmlpp::Element*>(doc.node())->cobj();
862 for (auto ns = root->nsDef; ns != nullptr; ns = ns->next) {
868 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
870 verify_interop_text_asset(context, interop);
871 if (namespace_count(asset, "DCSubtitle") > 1) {
872 context.warning(VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id());
876 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
878 verify_smpte_timed_text_asset(context, smpte, reel_asset_duration);
879 verify_smpte_subtitle_asset(context, smpte);
880 /* This asset may be encrypted and in that case we'll have no raw_xml() */
881 if (asset->raw_xml() && namespace_count(asset, "SubtitleReel") > 1) {
882 context.warning(VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id());
888 /** Verify all closed caption stuff */
890 verify_closed_caption_asset (
892 shared_ptr<const SubtitleAsset> asset,
893 optional<int64_t> reel_asset_duration
896 context.stage("Checking closed caption XML", asset->file());
897 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
898 * gets passed through libdcp which may clean up and therefore hide errors.
900 auto raw_xml = asset->raw_xml();
902 validate_xml(context, *raw_xml);
903 if (raw_xml->size() > 256 * 1024) {
904 context.bv21_error(VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file());
907 context.warning(VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED);
910 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
912 verify_interop_text_asset(context, interop);
915 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
917 verify_smpte_timed_text_asset(context, smpte, reel_asset_duration);
922 /** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes etc. */
925 verify_text_details (
927 vector<shared_ptr<Reel>> reels,
929 std::function<bool (shared_ptr<Reel>)> check,
930 std::function<optional<string> (shared_ptr<Reel>)> xml,
931 std::function<int64_t (shared_ptr<Reel>)> duration,
932 std::function<std::string (shared_ptr<Reel>)> id
935 /* end of last subtitle (in editable units) */
936 optional<int64_t> last_out;
937 auto too_short = false;
938 auto too_close = false;
939 auto too_early = false;
940 auto reel_overlap = false;
941 auto empty_text = false;
942 /* current reel start time (in editable units) */
943 int64_t reel_offset = 0;
944 optional<string> missing_load_font_id;
946 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool, bool&, vector<string>&)> parse;
948 parse = [&parse, &last_out, &too_short, &too_close, &too_early, &empty_text, &reel_offset, &missing_load_font_id](
949 cxml::ConstNodePtr node,
951 optional<Time> start_time,
955 vector<string>& font_ids
957 if (node->name() == "Subtitle") {
958 Time in (node->string_attribute("TimeIn"), tcr);
962 Time out (node->string_attribute("TimeOut"), tcr);
966 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
969 auto length = out - in;
970 if (length.as_editable_units_ceil(er) < 15) {
974 /* XXX: this feels dubious - is it really what Bv2.1 means? */
975 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
976 if (distance >= 0 && distance < 2) {
980 last_out = reel_offset + out.as_editable_units_floor(er);
981 } else if (node->name() == "Text") {
982 std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
983 if (!node->content().empty()) {
986 for (auto i: node->node_children()) {
987 if (node_has_content(i)) {
993 if (!node_has_content(node)) {
997 } else if (node->name() == "LoadFont") {
998 if (auto const id = node->optional_string_attribute("Id")) {
999 font_ids.push_back(*id);
1000 } else if (auto const id = node->optional_string_attribute("ID")) {
1001 font_ids.push_back(*id);
1003 } else if (node->name() == "Font") {
1004 if (auto const font_id = node->optional_string_attribute("Id")) {
1005 if (std::find_if(font_ids.begin(), font_ids.end(), [font_id](string const& id) { return id == font_id; }) == font_ids.end()) {
1006 missing_load_font_id = font_id;
1010 for (auto i: node->node_children()) {
1011 parse(i, tcr, start_time, er, first_reel, has_text, font_ids);
1015 for (auto i = 0U; i < reels.size(); ++i) {
1016 if (!check(reels[i])) {
1020 auto reel_xml = xml(reels[i]);
1022 context.warning(VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED);
1026 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1027 * read in by libdcp's parser.
1030 shared_ptr<cxml::Document> doc;
1032 optional<Time> start_time;
1033 switch (context.dcp->standard().get_value_or(dcp::Standard::SMPTE)) {
1034 case dcp::Standard::INTEROP:
1035 doc = make_shared<cxml::Document>("DCSubtitle");
1036 doc->read_string (*reel_xml);
1038 case dcp::Standard::SMPTE:
1039 doc = make_shared<cxml::Document>("SubtitleReel");
1040 doc->read_string (*reel_xml);
1041 tcr = doc->number_child<int>("TimeCodeRate");
1042 if (auto start_time_string = doc->optional_string_child("StartTime")) {
1043 start_time = Time(*start_time_string, tcr);
1047 bool has_text = false;
1048 vector<string> font_ids;
1049 parse(doc, tcr, start_time, edit_rate, i == 0, has_text, font_ids);
1050 auto end = reel_offset + duration(reels[i]);
1051 if (last_out && *last_out > end) {
1052 reel_overlap = true;
1056 if (context.dcp->standard() && *context.dcp->standard() == dcp::Standard::SMPTE && has_text && font_ids.empty()) {
1057 context.add_note(dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT).set_id(id(reels[i])));
1061 if (last_out && *last_out > reel_offset) {
1062 reel_overlap = true;
1066 context.warning(VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME);
1070 context.warning(VerificationNote::Code::INVALID_SUBTITLE_DURATION);
1074 context.warning(VerificationNote::Code::INVALID_SUBTITLE_SPACING);
1078 context.error(VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY);
1082 context.warning(VerificationNote::Code::EMPTY_TEXT);
1085 if (missing_load_font_id) {
1086 context.add_note(dcp::VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT).set_id(*missing_load_font_id));
1093 verify_closed_caption_details(Context& context, vector<shared_ptr<Reel>> reels)
1095 std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
1096 find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
1097 for (auto i: node->node_children()) {
1098 if (i->name() == "Text") {
1099 text_or_image.push_back (i);
1101 find_text_or_image (i, text_or_image);
1106 auto mismatched_valign = false;
1107 auto incorrect_order = false;
1109 std::function<void (cxml::ConstNodePtr)> parse;
1110 parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
1111 if (node->name() == "Subtitle") {
1112 vector<cxml::ConstNodePtr> text_or_image;
1113 find_text_or_image (node, text_or_image);
1114 optional<string> last_valign;
1115 optional<float> last_vpos;
1116 for (auto i: text_or_image) {
1117 auto valign = i->optional_string_attribute("VAlign");
1119 valign = i->optional_string_attribute("Valign").get_value_or("center");
1121 auto vpos = i->optional_number_attribute<float>("VPosition");
1123 vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
1127 if (*last_valign != valign) {
1128 mismatched_valign = true;
1131 last_valign = valign;
1133 if (!mismatched_valign) {
1135 if (*last_valign == "top" || *last_valign == "center") {
1136 if (*vpos < *last_vpos) {
1137 incorrect_order = true;
1140 if (*vpos > *last_vpos) {
1141 incorrect_order = true;
1150 for (auto i: node->node_children()) {
1155 for (auto reel: reels) {
1156 for (auto ccap: reel->closed_captions()) {
1157 auto reel_xml = ccap->asset()->raw_xml();
1159 context.warning(VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED);
1163 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1164 * read in by libdcp's parser.
1167 shared_ptr<cxml::Document> doc;
1169 optional<Time> start_time;
1171 doc = make_shared<cxml::Document>("SubtitleReel");
1172 doc->read_string (*reel_xml);
1174 doc = make_shared<cxml::Document>("DCSubtitle");
1175 doc->read_string (*reel_xml);
1181 if (mismatched_valign) {
1182 context.error(VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN);
1185 if (incorrect_order) {
1186 context.error(VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING);
1191 struct LinesCharactersResult
1193 bool warning_length_exceeded = false;
1194 bool error_length_exceeded = false;
1195 bool line_count_exceeded = false;
1201 verify_text_lines_and_characters (
1202 shared_ptr<SubtitleAsset> asset,
1205 LinesCharactersResult* result
1211 Event (Time time_, float position_, int characters_)
1213 , position (position_)
1214 , characters (characters_)
1217 Event (Time time_, shared_ptr<Event> start_)
1223 int position; //< position from 0 at top of screen to 100 at bottom
1225 shared_ptr<Event> start;
1228 vector<shared_ptr<Event>> events;
1230 auto position = [](shared_ptr<const SubtitleString> sub) {
1231 switch (sub->v_align()) {
1233 return lrintf(sub->v_position() * 100);
1234 case VAlign::CENTER:
1235 return lrintf((0.5f + sub->v_position()) * 100);
1236 case VAlign::BOTTOM:
1237 return lrintf((1.0f - sub->v_position()) * 100);
1243 for (auto j: asset->subtitles()) {
1244 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1246 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1247 events.push_back(in);
1248 events.push_back(make_shared<Event>(text->out(), in));
1252 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1253 return a->time < b->time;
1256 map<int, int> current;
1257 for (auto i: events) {
1258 if (current.size() > 3) {
1259 result->line_count_exceeded = true;
1261 for (auto j: current) {
1262 if (j.second > warning_length) {
1263 result->warning_length_exceeded = true;
1265 if (j.second > error_length) {
1266 result->error_length_exceeded = true;
1271 /* end of a subtitle */
1272 DCP_ASSERT (current.find(i->start->position) != current.end());
1273 if (current[i->start->position] == i->start->characters) {
1274 current.erase(i->start->position);
1276 current[i->start->position] -= i->start->characters;
1279 /* start of a subtitle */
1280 if (current.find(i->position) == current.end()) {
1281 current[i->position] = i->characters;
1283 current[i->position] += i->characters;
1292 verify_text_details(Context& context, vector<shared_ptr<Reel>> reels)
1294 if (reels.empty()) {
1298 if (reels[0]->main_subtitle() && reels[0]->main_subtitle()->asset_ref().resolved()) {
1299 verify_text_details(context, reels, reels[0]->main_subtitle()->edit_rate().numerator,
1300 [](shared_ptr<Reel> reel) {
1301 return static_cast<bool>(reel->main_subtitle());
1303 [](shared_ptr<Reel> reel) {
1304 return reel->main_subtitle()->asset()->raw_xml();
1306 [](shared_ptr<Reel> reel) {
1307 return reel->main_subtitle()->actual_duration();
1309 [](shared_ptr<Reel> reel) {
1310 return reel->main_subtitle()->id();
1315 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1316 verify_text_details(context, reels, reels[0]->closed_captions()[i]->edit_rate().numerator,
1317 [i](shared_ptr<Reel> reel) {
1318 return i < reel->closed_captions().size();
1320 [i](shared_ptr<Reel> reel) {
1321 return reel->closed_captions()[i]->asset()->raw_xml();
1323 [i](shared_ptr<Reel> reel) {
1324 return reel->closed_captions()[i]->actual_duration();
1326 [i](shared_ptr<Reel> reel) {
1327 return reel->closed_captions()[i]->id();
1332 verify_closed_caption_details(context, reels);
1337 verify_extension_metadata(Context& context, shared_ptr<const CPL> cpl)
1339 DCP_ASSERT (cpl->file());
1340 cxml::Document doc ("CompositionPlaylist");
1341 doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1343 auto missing = false;
1346 if (auto reel_list = doc.node_child("ReelList")) {
1347 auto reels = reel_list->node_children("Reel");
1348 if (!reels.empty()) {
1349 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1350 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1351 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1353 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1354 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1358 if (auto name = extension->optional_node_child("Name")) {
1359 if (name->content() != "Application") {
1360 malformed = "<Name> should be 'Application'";
1363 if (auto property_list = extension->optional_node_child("PropertyList")) {
1364 if (auto property = property_list->optional_node_child("Property")) {
1365 if (auto name = property->optional_node_child("Name")) {
1366 if (name->content() != "DCP Constraints Profile") {
1367 malformed = "<Name> property should be 'DCP Constraints Profile'";
1370 if (auto value = property->optional_node_child("Value")) {
1371 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1372 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1387 context.bv21_error(VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->file().get());
1388 } else if (!malformed.empty()) {
1389 context.bv21_error(VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get());
1395 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1397 vector<string> encrypted;
1398 for (auto i: dcp->cpls()) {
1399 for (auto j: i->reel_file_assets()) {
1400 if (j->asset_ref().resolved()) {
1401 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1402 if (mxf && mxf->encrypted()) {
1403 encrypted.push_back(j->asset_ref().id());
1409 for (auto i: pkl->assets()) {
1410 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1423 shared_ptr<const Reel> reel,
1424 int64_t start_frame,
1425 optional<dcp::Size> main_picture_active_area,
1426 bool* have_main_subtitle,
1427 bool* have_no_main_subtitle,
1428 size_t* most_closed_captions,
1429 size_t* fewest_closed_captions,
1430 map<Marker, Time>* markers_seen
1433 for (auto i: reel->assets()) {
1434 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1435 context.error(VerificationNote::Code::INVALID_DURATION, i->id());
1437 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1438 context.error(VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id());
1440 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1441 if (i->encryptable() && !file_asset->hash()) {
1442 context.bv21_error(VerificationNote::Code::MISSING_HASH, i->id());
1446 if (context.dcp->standard() == Standard::SMPTE) {
1447 boost::optional<int64_t> duration;
1448 for (auto i: reel->assets()) {
1450 duration = i->actual_duration();
1451 } else if (*duration != i->actual_duration()) {
1452 context.bv21_error(VerificationNote::Code::MISMATCHED_ASSET_DURATION);
1458 if (reel->main_picture()) {
1459 /* Check reel stuff */
1460 auto const frame_rate = reel->main_picture()->frame_rate();
1461 if (frame_rate.denominator != 1 ||
1462 (frame_rate.numerator != 24 &&
1463 frame_rate.numerator != 25 &&
1464 frame_rate.numerator != 30 &&
1465 frame_rate.numerator != 48 &&
1466 frame_rate.numerator != 50 &&
1467 frame_rate.numerator != 60 &&
1468 frame_rate.numerator != 96)) {
1469 context.error(VerificationNote::Code::INVALID_PICTURE_FRAME_RATE, String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator));
1472 if (reel->main_picture()->asset_ref().resolved()) {
1473 verify_main_picture_asset(context, reel->main_picture(), start_frame);
1474 auto const asset_size = reel->main_picture()->asset()->size();
1475 if (main_picture_active_area) {
1476 if (main_picture_active_area->width > asset_size.width) {
1478 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1479 String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1480 context.cpl->file().get()
1483 if (main_picture_active_area->height > asset_size.height) {
1485 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1486 String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1487 context.cpl->file().get()
1495 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1496 verify_main_sound_asset(context, reel->main_sound());
1499 if (reel->main_subtitle()) {
1500 verify_main_subtitle_reel(context, reel->main_subtitle());
1501 if (reel->main_subtitle()->asset_ref().resolved()) {
1502 verify_subtitle_asset(context, reel->main_subtitle()->asset(), reel->main_subtitle()->duration());
1504 *have_main_subtitle = true;
1506 *have_no_main_subtitle = true;
1509 for (auto i: reel->closed_captions()) {
1510 verify_closed_caption_reel(context, i);
1511 if (i->asset_ref().resolved()) {
1512 verify_closed_caption_asset(context, i->asset(), i->duration());
1516 if (reel->main_markers()) {
1517 for (auto const& i: reel->main_markers()->get()) {
1518 markers_seen->insert(i);
1520 if (reel->main_markers()->entry_point()) {
1521 context.error(VerificationNote::Code::UNEXPECTED_ENTRY_POINT);
1523 if (reel->main_markers()->duration()) {
1524 context.error(VerificationNote::Code::UNEXPECTED_DURATION);
1528 *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1529 *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1536 verify_cpl(Context& context, shared_ptr<const CPL> cpl)
1538 context.stage("Checking CPL", cpl->file());
1539 validate_xml(context, cpl->file().get());
1541 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1542 context.bv21_error(VerificationNote::Code::PARTIALLY_ENCRYPTED);
1543 } else if (cpl->all_encrypted()) {
1544 context.ok(VerificationNote::Code::ALL_ENCRYPTED);
1545 } else if (!cpl->all_encrypted()) {
1546 context.ok(VerificationNote::Code::NONE_ENCRYPTED);
1549 for (auto const& i: cpl->additional_subtitle_languages()) {
1550 verify_language_tag(context, i);
1553 if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1554 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1555 * of the approved ones.
1557 auto all = ContentKind::all();
1558 auto name = cpl->content_kind().name();
1559 transform(name.begin(), name.end(), name.begin(), ::tolower);
1560 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1561 if (iter == all.end()) {
1562 context.error(VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name());
1566 if (cpl->release_territory()) {
1567 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") {
1568 auto terr = cpl->release_territory().get();
1569 /* Must be a valid region tag, or "001" */
1571 LanguageTag::RegionSubtag test(terr);
1573 if (terr != "001") {
1574 context.bv21_error(VerificationNote::Code::INVALID_LANGUAGE, terr);
1580 for (auto version: cpl->content_versions()) {
1581 if (version.label_text.empty()) {
1582 context.warning(VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT, cpl->file().get());
1587 if (context.dcp->standard() == Standard::SMPTE) {
1588 if (!cpl->annotation_text()) {
1589 context.bv21_error(VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->file().get());
1590 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1591 context.warning(VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->file().get());
1595 for (auto i: context.dcp->pkls()) {
1596 /* Check that the CPL's hash corresponds to the PKL */
1597 optional<string> h = i->hash(cpl->id());
1598 auto calculated_cpl_hash = make_digest(ArrayData(*cpl->file()));
1599 if (h && calculated_cpl_hash != *h) {
1601 dcp::VerificationNote(
1602 VerificationNote::Type::ERROR,
1603 VerificationNote::Code::MISMATCHED_CPL_HASHES,
1605 ).set_calculated_hash(calculated_cpl_hash).set_reference_hash(*h)
1609 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1610 optional<string> required_annotation_text;
1611 for (auto j: i->assets()) {
1612 /* See if this is a CPL */
1613 for (auto k: context.dcp->cpls()) {
1614 if (j->id() == k->id()) {
1615 if (!required_annotation_text) {
1616 /* First CPL we have found; this is the required AnnotationText unless we find another */
1617 required_annotation_text = cpl->content_title_text();
1619 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1620 required_annotation_text = boost::none;
1626 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1627 context.bv21_error(VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get());
1631 /* set to true if any reel has a MainSubtitle */
1632 auto have_main_subtitle = false;
1633 /* set to true if any reel has no MainSubtitle */
1634 auto have_no_main_subtitle = false;
1635 /* fewest number of closed caption assets seen in a reel */
1636 size_t fewest_closed_captions = SIZE_MAX;
1637 /* most number of closed caption assets seen in a reel */
1638 size_t most_closed_captions = 0;
1639 map<Marker, Time> markers_seen;
1641 auto const main_picture_active_area = cpl->main_picture_active_area();
1642 if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1644 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1645 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1649 if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1651 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1652 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1658 for (auto reel: cpl->reels()) {
1659 context.stage("Checking reel", optional<boost::filesystem::path>());
1664 main_picture_active_area,
1665 &have_main_subtitle,
1666 &have_no_main_subtitle,
1667 &most_closed_captions,
1668 &fewest_closed_captions,
1671 frame += reel->duration();
1674 verify_text_details(context, cpl->reels());
1676 if (context.dcp->standard() == Standard::SMPTE) {
1677 if (auto msc = cpl->main_sound_configuration()) {
1678 if (context.audio_channels && msc->channels() != *context.audio_channels) {
1680 VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION,
1681 String::compose("MainSoundConfiguration has %1 channels but sound assets have %2", msc->channels(), *context.audio_channels),
1687 if (have_main_subtitle && have_no_main_subtitle) {
1688 context.bv21_error(VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS);
1691 if (fewest_closed_captions != most_closed_captions) {
1692 context.bv21_error(VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS);
1695 if (cpl->content_kind() == ContentKind::FEATURE) {
1696 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1697 context.bv21_error(VerificationNote::Code::MISSING_FFEC_IN_FEATURE);
1699 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1700 context.bv21_error(VerificationNote::Code::MISSING_FFMC_IN_FEATURE);
1704 auto ffoc = markers_seen.find(Marker::FFOC);
1705 if (ffoc == markers_seen.end()) {
1706 context.warning(VerificationNote::Code::MISSING_FFOC);
1707 } else if (ffoc->second.e != 1) {
1708 context.warning(VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e));
1711 auto lfoc = markers_seen.find(Marker::LFOC);
1712 if (lfoc == markers_seen.end()) {
1713 context.warning(VerificationNote::Code::MISSING_LFOC);
1715 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1716 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1717 context.warning(VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time));
1721 LinesCharactersResult result;
1722 for (auto reel: cpl->reels()) {
1723 if (reel->main_subtitle() && reel->main_subtitle()->asset_ref().resolved()) {
1724 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1728 if (result.line_count_exceeded) {
1729 context.warning(VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT);
1731 if (result.error_length_exceeded) {
1732 context.warning(VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH);
1733 } else if (result.warning_length_exceeded) {
1734 context.warning(VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH);
1737 result = LinesCharactersResult();
1738 for (auto reel: cpl->reels()) {
1739 for (auto i: reel->closed_captions()) {
1741 verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1746 if (result.line_count_exceeded) {
1747 context.bv21_error(VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT);
1749 if (result.error_length_exceeded) {
1750 context.bv21_error(VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH);
1753 if (!cpl->read_composition_metadata()) {
1754 context.bv21_error(VerificationNote::Code::MISSING_CPL_METADATA, cpl->file().get());
1755 } else if (!cpl->version_number()) {
1756 context.bv21_error(VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->file().get());
1759 verify_extension_metadata(context, cpl);
1761 if (cpl->any_encrypted()) {
1762 cxml::Document doc("CompositionPlaylist");
1763 DCP_ASSERT(cpl->file());
1764 doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1765 if (!doc.optional_node_child("Signature")) {
1766 context.bv21_error(VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->file().get());
1775 verify_pkl(Context& context, shared_ptr<const PKL> pkl)
1777 validate_xml(context, pkl->file().get());
1779 if (pkl_has_encrypted_assets(context.dcp, pkl)) {
1780 cxml::Document doc("PackingList");
1781 doc.read_file(dcp::filesystem::fix_long_path(pkl->file().get()));
1782 if (!doc.optional_node_child("Signature")) {
1783 context.bv21_error(VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get());
1787 set<string> uuid_set;
1788 for (auto asset: pkl->assets()) {
1789 if (!uuid_set.insert(asset->id()).second) {
1790 context.error(VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get());
1800 verify_assetmap(Context& context, shared_ptr<const DCP> dcp)
1802 auto asset_map = dcp->asset_map();
1803 DCP_ASSERT(asset_map);
1805 validate_xml(context, asset_map->file().get());
1807 set<string> uuid_set;
1808 for (auto const& asset: asset_map->assets()) {
1809 if (!uuid_set.insert(asset.id()).second) {
1810 context.error(VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get());
1819 vector<boost::filesystem::path> directories,
1820 vector<dcp::DecryptedKDM> kdms,
1821 function<void (string, optional<boost::filesystem::path>)> stage,
1822 function<void (float)> progress,
1823 VerificationOptions options,
1824 optional<boost::filesystem::path> xsd_dtd_directory
1827 if (!xsd_dtd_directory) {
1828 xsd_dtd_directory = resources_directory() / "xsd";
1830 *xsd_dtd_directory = filesystem::canonical(*xsd_dtd_directory);
1832 vector<VerificationNote> notes;
1833 Context context(notes, *xsd_dtd_directory, stage, progress, options);
1835 vector<shared_ptr<DCP>> dcps;
1836 for (auto i: directories) {
1837 dcps.push_back (make_shared<DCP>(i));
1840 for (auto dcp: dcps) {
1841 stage ("Checking DCP", dcp->directory());
1845 bool carry_on = true;
1847 dcp->read (¬es, true);
1848 } catch (MissingAssetmapError& e) {
1849 context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1851 } catch (ReadError& e) {
1852 context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1853 } catch (XMLError& e) {
1854 context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1855 } catch (MXFFileError& e) {
1856 context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1857 } catch (BadURNUUIDError& e) {
1858 context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1859 } catch (cxml::Error& e) {
1860 context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1867 if (dcp->standard() != Standard::SMPTE) {
1868 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1871 for (auto kdm: kdms) {
1875 for (auto cpl: dcp->cpls()) {
1878 verify_cpl(context, cpl);
1879 context.cpl.reset();
1880 } catch (ReadError& e) {
1881 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1885 for (auto pkl: dcp->pkls()) {
1886 stage("Checking PKL", pkl->file());
1887 verify_pkl(context, pkl);
1890 if (dcp->asset_map_file()) {
1891 stage("Checking ASSETMAP", dcp->asset_map_file().get());
1892 verify_assetmap(context, dcp);
1894 context.error(VerificationNote::Code::MISSING_ASSETMAP);
1898 return { notes, dcps };
1903 dcp::note_to_string (VerificationNote note)
1905 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1907 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1908 * not "ClosedCaption assets must have an <EntryPoint> tag."
1910 * It's OK to use XML tag names where they are clear.
1911 * If both ID and filename are available, use only the ID.
1912 * End messages with a full stop.
1913 * Messages should not mention whether or not their errors are a part of Bv2.1.
1915 switch (note.code()) {
1916 case VerificationNote::Code::FAILED_READ:
1917 return *note.note();
1918 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1919 return String::compose("The hash (%1) of the CPL (%2) in the PKL does not agree with the CPL file (%3).", note.reference_hash().get(), note.cpl_id().get(), note.calculated_hash().get());
1920 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1921 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1922 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1923 return String::compose("The hash (%1) of the picture asset %2 does not agree with the PKL file (%3).", note.calculated_hash().get(), note.file()->filename(), note.reference_hash().get());
1924 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1925 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1926 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1927 return String::compose("The hash (%1) of the sound asset %2 does not agree with the PKL file (%3).", note.calculated_hash().get(), note.file()->filename(), note.reference_hash().get());
1928 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1929 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1930 case VerificationNote::Code::EMPTY_ASSET_PATH:
1931 return "The asset map contains an empty asset path.";
1932 case VerificationNote::Code::MISSING_ASSET:
1933 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1934 case VerificationNote::Code::MISMATCHED_STANDARD:
1935 return "The DCP contains both SMPTE and Interop parts.";
1936 case VerificationNote::Code::INVALID_XML:
1937 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1938 case VerificationNote::Code::MISSING_ASSETMAP:
1939 return "No valid ASSETMAP or ASSETMAP.xml was found.";
1940 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1941 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1942 case VerificationNote::Code::INVALID_DURATION:
1943 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1944 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1945 return String::compose(
1946 "Frame %1 (timecode %2) in asset %3 has an instantaneous bit rate that is larger than the limit of 250Mbit/s.",
1948 dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
1949 note.file()->filename()
1951 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1952 return String::compose(
1953 "Frame %1 (timecode %2) in asset %3 has an instantaneous bit rate that is close to the limit of 250Mbit/s.",
1955 dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
1956 note.file()->filename()
1958 case VerificationNote::Code::EXTERNAL_ASSET:
1959 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());
1960 case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1961 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1962 case VerificationNote::Code::INVALID_STANDARD:
1963 return "This DCP does not use the SMPTE standard.";
1964 case VerificationNote::Code::INVALID_LANGUAGE:
1965 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1966 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1967 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1968 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1969 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1970 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1971 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1972 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1973 return "3D 4K DCPs are not allowed.";
1974 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1975 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1976 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1977 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1978 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1979 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());
1980 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1981 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1982 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1983 return "Some subtitle assets have different <Language> tags than others";
1984 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1985 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1986 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1987 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1988 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1989 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1990 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1991 return "At least one subtitle lasts less than 15 frames.";
1992 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1993 return "At least one pair of subtitles is separated by less than 2 frames.";
1994 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1995 return "At least one subtitle extends outside of its reel.";
1996 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1997 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1998 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1999 return "There are more than 52 characters in at least one subtitle line.";
2000 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
2001 return "There are more than 79 characters in at least one subtitle line.";
2002 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
2003 return "There are more than 3 closed caption lines in at least one place.";
2004 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
2005 return "There are more than 32 characters in at least one closed caption line.";
2006 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
2007 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
2008 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
2009 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.cpl_id().get());
2010 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
2011 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.cpl_id().get());
2012 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
2013 return "All assets in a reel do not have the same duration.";
2014 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
2015 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
2016 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
2017 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
2018 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
2019 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
2020 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
2021 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
2022 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
2023 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
2024 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
2025 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
2026 case VerificationNote::Code::MISSING_HASH:
2027 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
2028 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
2029 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
2030 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
2031 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
2032 case VerificationNote::Code::MISSING_FFOC:
2033 return "There should be a FFOC (first frame of content) marker.";
2034 case VerificationNote::Code::MISSING_LFOC:
2035 return "There should be a LFOC (last frame of content) marker.";
2036 case VerificationNote::Code::INCORRECT_FFOC:
2037 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
2038 case VerificationNote::Code::INCORRECT_LFOC:
2039 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
2040 case VerificationNote::Code::MISSING_CPL_METADATA:
2041 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.cpl_id().get());
2042 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
2043 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.cpl_id().get());
2044 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
2045 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.cpl_id().get());
2046 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
2047 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
2048 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
2049 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.cpl_id().get());
2050 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
2051 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
2052 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
2053 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
2054 case VerificationNote::Code::ALL_ENCRYPTED:
2055 return "All the assets are encrypted.";
2056 case VerificationNote::Code::NONE_ENCRYPTED:
2057 return "All the assets are unencrypted.";
2058 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
2059 return "Some assets are encrypted but some are not.";
2060 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
2061 return String::compose(
2062 "Frame %1 (timecode %2) has an invalid JPEG2000 codestream (%3).",
2064 dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
2067 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
2068 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
2069 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
2070 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
2071 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
2072 return "The JPEG2000 tile size is not the same as the image size.";
2073 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
2074 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
2075 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
2076 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
2077 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
2078 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
2079 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
2080 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
2081 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
2082 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
2083 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
2084 return "POC marker found outside main header.";
2085 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
2086 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
2087 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
2088 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
2089 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
2090 return "No TLM marker was found in a JPEG2000 codestream.";
2091 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
2092 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
2093 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
2094 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
2095 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
2097 vector<string> parts;
2098 boost::split (parts, note.note().get(), boost::is_any_of(" "));
2099 DCP_ASSERT (parts.size() == 2);
2100 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]);
2102 case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
2103 return "Some aspect of this DCP could not be checked because it is encrypted.";
2104 case VerificationNote::Code::EMPTY_TEXT:
2105 return "There is an empty <Text> node in a subtitle or closed caption.";
2106 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
2107 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
2108 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
2109 return "Some closed captions are not listed in the order of their vertical position.";
2110 case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
2111 return "There is an <EntryPoint> node inside a <MainMarkers>.";
2112 case VerificationNote::Code::UNEXPECTED_DURATION:
2113 return "There is an <Duration> node inside a <MainMarkers>.";
2114 case VerificationNote::Code::INVALID_CONTENT_KIND:
2115 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
2116 case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
2117 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
2118 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
2119 return String::compose("The PKL %1 has more than one asset with the same ID.", note.note().get());
2120 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
2121 return String::compose("The ASSETMAP %1 has more than one asset with the same ID.", note.note().get());
2122 case VerificationNote::Code::MISSING_SUBTITLE:
2123 return String::compose("The subtitle asset %1 has no subtitles.", note.note().get());
2124 case VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
2125 return String::compose("<IssueDate> has an invalid value: %1", note.note().get());
2126 case VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS:
2127 return String::compose("The sound assets do not all have the same channel count; the first to differ is %1", note.file()->filename());
2128 case VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION:
2129 return String::compose("<MainSoundConfiguration> has an invalid value: %1", note.note().get());
2130 case VerificationNote::Code::MISSING_FONT:
2131 return String::compose("The font file for font ID \"%1\" was not found, or was not referred to in the ASSETMAP.", note.note().get());
2132 case VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE:
2133 return String::compose(
2134 "Frame %1 has an image component that is too large (component %2 is %3 bytes in size).",
2135 note.frame().get(), note.component().get(), note.size().get()
2137 case VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT:
2138 return String::compose("The XML in the subtitle asset %1 has more than one namespace declaration.", note.note().get());
2139 case VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT:
2140 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());
2141 case VerificationNote::Code::MISSING_LOAD_FONT:
2142 return String::compose("The SMPTE subtitle asset %1 has <Text> nodes but no <LoadFont> node", note.id().get());
2143 case VerificationNote::Code::MISMATCHED_ASSET_MAP_ID:
2144 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());
2145 case VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT:
2146 return String::compose("The <LabelText> in a <ContentVersion> in CPL %1 is empty", note.cpl_id().get());
2154 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2156 return a.type() == b.type() &&
2157 a.code() == b.code() &&
2158 a.note() == b.note() &&
2159 a.file() == b.file() &&
2160 a.line() == b.line() &&
2161 a.frame() == b.frame() &&
2162 a.component() == b.component() &&
2163 a.size() == b.size() &&
2165 a.other_id() == b.other_id() &&
2166 a.frame_rate() == b.frame_rate() &&
2167 a.cpl_id() == b.cpl_id() &&
2168 a.reference_hash() == b.reference_hash() &&
2169 a.calculated_hash() == b.calculated_hash();
2174 dcp::operator!=(dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2181 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2183 if (a.type() != b.type()) {
2184 return a.type() < b.type();
2187 if (a.code() != b.code()) {
2188 return a.code() < b.code();
2191 if (a.note() != b.note()) {
2192 return a.note().get_value_or("") < b.note().get_value_or("");
2195 if (a.file() != b.file()) {
2196 return a.file().get_value_or("") < b.file().get_value_or("");
2199 if (a.line() != b.line()) {
2200 return a.line().get_value_or(0) < b.line().get_value_or(0);
2203 if (a.frame() != b.frame()) {
2204 return a.frame().get_value_or(0) < b.frame().get_value_or(0);
2207 if (a.component() != b.component()) {
2208 return a.component().get_value_or(0) < b.component().get_value_or(0);
2211 if (a.size() != b.size()) {
2212 return a.size().get_value_or(0) < b.size().get_value_or(0);
2215 if (a.id() != b.id()) {
2216 return a.id().get_value_or("") < b.id().get_value_or("");
2219 if (a.other_id() != b.other_id()) {
2220 return a.other_id().get_value_or("") < b.other_id().get_value_or("");
2223 return a.frame_rate().get_value_or(0) != b.frame_rate().get_value_or(0);
2228 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2230 s << note_to_string (note);
2232 s << " [" << note.note().get() << "]";
2235 s << " [" << note.file().get() << "]";
2238 s << " [" << note.line().get() << "]";