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 bool any_bad_frames_seen = false;
544 auto check_frame_size = [max_frame, risky_frame, file, start_frame, &any_bad_frames_seen](Context& context, int index, int size, int frame_rate) {
545 if (size > max_frame) {
548 VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
549 ).set_frame(start_frame + index).set_frame_rate(frame_rate)
551 any_bad_frames_seen = true;
552 } else if (size > risky_frame) {
555 VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
556 ).set_frame(start_frame + index).set_frame_rate(frame_rate)
558 any_bad_frames_seen = true;
562 if (auto mono_asset = dynamic_pointer_cast<MonoPictureAsset>(reel_file_asset->asset_ref().asset())) {
563 auto reader = mono_asset->start_read ();
564 for (int64_t i = 0; i < duration; ++i) {
565 auto frame = reader->get_frame (i);
566 check_frame_size(context, i, frame->size(), mono_asset->frame_rate().numerator);
567 if (!mono_asset->encrypted() || mono_asset->key()) {
568 vector<VerificationNote> j2k_notes;
569 verify_j2k(frame, start_frame, i, mono_asset->frame_rate().numerator, j2k_notes);
570 check_and_add (j2k_notes);
572 context.progress(float(i) / duration);
574 } else if (auto stereo_asset = dynamic_pointer_cast<StereoPictureAsset>(asset)) {
575 auto reader = stereo_asset->start_read ();
576 for (int64_t i = 0; i < duration; ++i) {
577 auto frame = reader->get_frame (i);
578 check_frame_size(context, i, frame->left()->size(), stereo_asset->frame_rate().numerator);
579 check_frame_size(context, i, frame->right()->size(), stereo_asset->frame_rate().numerator);
580 if (!stereo_asset->encrypted() || stereo_asset->key()) {
581 vector<VerificationNote> j2k_notes;
582 verify_j2k(frame->left(), start_frame, i, stereo_asset->frame_rate().numerator, j2k_notes);
583 verify_j2k(frame->right(), start_frame, i, stereo_asset->frame_rate().numerator, j2k_notes);
584 check_and_add (j2k_notes);
586 context.progress(float(i) / duration);
591 if (!any_bad_frames_seen) {
592 context.ok(VerificationNote::Code::VALID_PICTURE_FRAME_SIZES_IN_BYTES, file);
598 verify_main_picture_asset(Context& context, shared_ptr<const ReelPictureAsset> reel_asset, int64_t start_frame)
600 auto asset = reel_asset->asset();
601 auto const file = *asset->file();
603 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)) {
604 context.stage("Checking picture asset hash", file);
605 string reference_hash;
606 string calculated_hash;
607 auto const r = verify_asset(context, reel_asset, &reference_hash, &calculated_hash);
609 case VerifyAssetResult::BAD:
611 dcp::VerificationNote(
612 VerificationNote::Type::ERROR,
613 VerificationNote::Code::INCORRECT_PICTURE_HASH,
615 ).set_reference_hash(reference_hash).set_calculated_hash(calculated_hash)
618 case VerifyAssetResult::CPL_PKL_DIFFER:
619 context.error(VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file);
626 context.stage("Checking picture frame sizes", asset->file());
627 verify_picture_asset(context, reel_asset, file, start_frame);
629 /* Only flat/scope allowed by Bv2.1 */
631 asset->size() != Size(2048, 858) &&
632 asset->size() != Size(1998, 1080) &&
633 asset->size() != Size(4096, 1716) &&
634 asset->size() != Size(3996, 2160)) {
635 context.bv21_error(VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS, String::compose("%1x%2", asset->size().width, asset->size().height), file);
638 /* Only 24, 25, 48fps allowed for 2K */
640 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
641 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
644 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
645 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
650 if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
651 /* Only 24fps allowed for 4K */
652 if (asset->edit_rate() != Fraction(24, 1)) {
654 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
655 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
660 /* Only 2D allowed for 4K */
661 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
663 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
664 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
674 verify_main_sound_asset(Context& context, shared_ptr<const ReelSoundAsset> reel_asset)
676 auto asset = reel_asset->asset();
677 auto const file = *asset->file();
679 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)) {
680 context.stage("Checking sound asset hash", file);
681 string reference_hash;
682 string calculated_hash;
683 auto const r = verify_asset(context, reel_asset, &reference_hash, &calculated_hash);
685 case VerifyAssetResult::BAD:
687 dcp::VerificationNote(
688 VerificationNote::Type::ERROR,
689 VerificationNote::Code::INCORRECT_SOUND_HASH,
691 ).set_reference_hash(reference_hash).set_calculated_hash(calculated_hash)
694 case VerifyAssetResult::CPL_PKL_DIFFER:
695 context.error(VerificationNote::Code::MISMATCHED_SOUND_HASHES, file);
702 if (!context.audio_channels) {
703 context.audio_channels = asset->channels();
704 } else if (*context.audio_channels != asset->channels()) {
705 context.error(VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS, file);
708 context.stage("Checking sound asset metadata", file);
710 if (auto lang = asset->language()) {
711 verify_language_tag(context, *lang);
713 if (asset->sampling_rate() != 48000) {
714 context.bv21_error(VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), file);
720 verify_main_subtitle_reel(Context& context, shared_ptr<const ReelSubtitleAsset> reel_asset)
722 /* XXX: is Language compulsory? */
723 if (reel_asset->language()) {
724 verify_language_tag(context, *reel_asset->language());
727 if (!reel_asset->entry_point()) {
728 context.bv21_error(VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id());
729 } else if (reel_asset->entry_point().get()) {
730 context.bv21_error(VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id());
736 verify_closed_caption_reel(Context& context, shared_ptr<const ReelClosedCaptionAsset> reel_asset)
738 /* XXX: is Language compulsory? */
739 if (reel_asset->language()) {
740 verify_language_tag(context, *reel_asset->language());
743 if (!reel_asset->entry_point()) {
744 context.bv21_error(VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id());
745 } else if (reel_asset->entry_point().get()) {
746 context.bv21_error(VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id());
751 /** Verify stuff that is common to both subtitles and closed captions */
753 verify_smpte_timed_text_asset (
755 shared_ptr<const SMPTESubtitleAsset> asset,
756 optional<int64_t> reel_asset_duration
759 if (asset->language()) {
760 verify_language_tag(context, *asset->language());
762 context.bv21_error(VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file());
765 auto const size = filesystem::file_size(asset->file().get());
766 if (size > 115 * 1024 * 1024) {
767 context.bv21_error(VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file());
770 /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
771 * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
773 auto fonts = asset->font_data ();
775 for (auto i: fonts) {
776 total_size += i.second.size();
778 if (total_size > 10 * 1024 * 1024) {
779 context.bv21_error(VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get());
782 if (!asset->start_time()) {
783 context.bv21_error(VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get());
784 } else if (asset->start_time() != Time()) {
785 context.bv21_error(VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get());
788 if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
790 VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
791 String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
798 /** Verify Interop subtitle / CCAP stuff */
800 verify_interop_text_asset(Context& context, shared_ptr<const InteropSubtitleAsset> asset)
802 if (asset->subtitles().empty()) {
803 context.error(VerificationNote::Code::MISSING_SUBTITLE, asset->id(), asset->file().get());
805 auto const unresolved = asset->unresolved_fonts();
806 if (!unresolved.empty()) {
807 context.error(VerificationNote::Code::MISSING_FONT, unresolved.front());
812 /** Verify SMPTE subtitle-only stuff */
814 verify_smpte_subtitle_asset(Context& context, shared_ptr<const SMPTESubtitleAsset> asset)
816 if (asset->language()) {
817 if (!context.subtitle_language) {
818 context.subtitle_language = *asset->language();
819 } else if (context.subtitle_language != *asset->language()) {
820 context.bv21_error(VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES);
824 DCP_ASSERT (asset->resource_id());
825 auto xml_id = asset->xml_id();
827 if (asset->resource_id().get() != xml_id) {
828 context.bv21_error(VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID);
831 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
832 context.bv21_error(VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID);
835 context.warning(VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED);
838 if (asset->raw_xml()) {
839 /* Deluxe require this in their QC even if it seems never to be mentioned in any standard */
840 cxml::Document doc("SubtitleReel");
841 doc.read_string(*asset->raw_xml());
842 auto issue_date = doc.string_child("IssueDate");
843 std::regex reg("^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d$");
844 if (!std::regex_match(issue_date, reg)) {
845 context.warning(VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, issue_date);
851 /** Verify all subtitle stuff */
853 verify_subtitle_asset(Context& context, shared_ptr<const SubtitleAsset> asset, optional<int64_t> reel_asset_duration)
855 context.stage("Checking subtitle XML", asset->file());
856 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
857 * gets passed through libdcp which may clean up and therefore hide errors.
859 if (asset->raw_xml()) {
860 validate_xml(context, asset->raw_xml().get());
862 context.warning(VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED);
865 auto namespace_count = [](shared_ptr<const SubtitleAsset> asset, string root_node) {
866 cxml::Document doc(root_node);
867 doc.read_string(asset->raw_xml().get());
868 auto root = dynamic_cast<xmlpp::Element*>(doc.node())->cobj();
870 for (auto ns = root->nsDef; ns != nullptr; ns = ns->next) {
876 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
878 verify_interop_text_asset(context, interop);
879 if (namespace_count(asset, "DCSubtitle") > 1) {
880 context.warning(VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id());
884 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
886 verify_smpte_timed_text_asset(context, smpte, reel_asset_duration);
887 verify_smpte_subtitle_asset(context, smpte);
888 /* This asset may be encrypted and in that case we'll have no raw_xml() */
889 if (asset->raw_xml() && namespace_count(asset, "SubtitleReel") > 1) {
890 context.warning(VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id());
896 /** Verify all closed caption stuff */
898 verify_closed_caption_asset (
900 shared_ptr<const SubtitleAsset> asset,
901 optional<int64_t> reel_asset_duration
904 context.stage("Checking closed caption XML", asset->file());
905 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
906 * gets passed through libdcp which may clean up and therefore hide errors.
908 auto raw_xml = asset->raw_xml();
910 validate_xml(context, *raw_xml);
911 if (raw_xml->size() > 256 * 1024) {
912 context.bv21_error(VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file());
915 context.warning(VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED);
918 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
920 verify_interop_text_asset(context, interop);
923 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
925 verify_smpte_timed_text_asset(context, smpte, reel_asset_duration);
930 /** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes etc. */
933 verify_text_details (
935 vector<shared_ptr<Reel>> reels,
937 std::function<bool (shared_ptr<Reel>)> check,
938 std::function<optional<string> (shared_ptr<Reel>)> xml,
939 std::function<int64_t (shared_ptr<Reel>)> duration,
940 std::function<std::string (shared_ptr<Reel>)> id
943 /* end of last subtitle (in editable units) */
944 optional<int64_t> last_out;
945 auto too_short = false;
946 auto too_close = false;
947 auto too_early = false;
948 auto reel_overlap = false;
949 auto empty_text = false;
950 /* current reel start time (in editable units) */
951 int64_t reel_offset = 0;
952 optional<string> missing_load_font_id;
954 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool, bool&, vector<string>&)> parse;
956 parse = [&parse, &last_out, &too_short, &too_close, &too_early, &empty_text, &reel_offset, &missing_load_font_id](
957 cxml::ConstNodePtr node,
959 optional<Time> start_time,
963 vector<string>& font_ids
965 if (node->name() == "Subtitle") {
966 Time in (node->string_attribute("TimeIn"), tcr);
970 Time out (node->string_attribute("TimeOut"), tcr);
974 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
977 auto length = out - in;
978 if (length.as_editable_units_ceil(er) < 15) {
982 /* XXX: this feels dubious - is it really what Bv2.1 means? */
983 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
984 if (distance >= 0 && distance < 2) {
988 last_out = reel_offset + out.as_editable_units_floor(er);
989 } else if (node->name() == "Text") {
990 std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
991 if (!node->content().empty()) {
994 for (auto i: node->node_children()) {
995 if (node_has_content(i)) {
1001 if (!node_has_content(node)) {
1005 } else if (node->name() == "LoadFont") {
1006 if (auto const id = node->optional_string_attribute("Id")) {
1007 font_ids.push_back(*id);
1008 } else if (auto const id = node->optional_string_attribute("ID")) {
1009 font_ids.push_back(*id);
1011 } else if (node->name() == "Font") {
1012 if (auto const font_id = node->optional_string_attribute("Id")) {
1013 if (std::find_if(font_ids.begin(), font_ids.end(), [font_id](string const& id) { return id == font_id; }) == font_ids.end()) {
1014 missing_load_font_id = font_id;
1018 for (auto i: node->node_children()) {
1019 parse(i, tcr, start_time, er, first_reel, has_text, font_ids);
1023 for (auto i = 0U; i < reels.size(); ++i) {
1024 if (!check(reels[i])) {
1028 auto reel_xml = xml(reels[i]);
1030 context.warning(VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED);
1034 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1035 * read in by libdcp's parser.
1038 shared_ptr<cxml::Document> doc;
1040 optional<Time> start_time;
1041 switch (context.dcp->standard().get_value_or(dcp::Standard::SMPTE)) {
1042 case dcp::Standard::INTEROP:
1043 doc = make_shared<cxml::Document>("DCSubtitle");
1044 doc->read_string (*reel_xml);
1046 case dcp::Standard::SMPTE:
1047 doc = make_shared<cxml::Document>("SubtitleReel");
1048 doc->read_string (*reel_xml);
1049 tcr = doc->number_child<int>("TimeCodeRate");
1050 if (auto start_time_string = doc->optional_string_child("StartTime")) {
1051 start_time = Time(*start_time_string, tcr);
1055 bool has_text = false;
1056 vector<string> font_ids;
1057 parse(doc, tcr, start_time, edit_rate, i == 0, has_text, font_ids);
1058 auto end = reel_offset + duration(reels[i]);
1059 if (last_out && *last_out > end) {
1060 reel_overlap = true;
1064 if (context.dcp->standard() && *context.dcp->standard() == dcp::Standard::SMPTE && has_text && font_ids.empty()) {
1065 context.add_note(dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT).set_id(id(reels[i])));
1069 if (last_out && *last_out > reel_offset) {
1070 reel_overlap = true;
1074 context.warning(VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME);
1078 context.warning(VerificationNote::Code::INVALID_SUBTITLE_DURATION);
1082 context.warning(VerificationNote::Code::INVALID_SUBTITLE_SPACING);
1086 context.error(VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY);
1090 context.warning(VerificationNote::Code::EMPTY_TEXT);
1093 if (missing_load_font_id) {
1094 context.add_note(dcp::VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT).set_id(*missing_load_font_id));
1101 verify_closed_caption_details(Context& context, vector<shared_ptr<Reel>> reels)
1103 std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
1104 find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
1105 for (auto i: node->node_children()) {
1106 if (i->name() == "Text") {
1107 text_or_image.push_back (i);
1109 find_text_or_image (i, text_or_image);
1114 auto mismatched_valign = false;
1115 auto incorrect_order = false;
1117 std::function<void (cxml::ConstNodePtr)> parse;
1118 parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
1119 if (node->name() == "Subtitle") {
1120 vector<cxml::ConstNodePtr> text_or_image;
1121 find_text_or_image (node, text_or_image);
1122 optional<string> last_valign;
1123 optional<float> last_vpos;
1124 for (auto i: text_or_image) {
1125 auto valign = i->optional_string_attribute("VAlign");
1127 valign = i->optional_string_attribute("Valign").get_value_or("center");
1129 auto vpos = i->optional_number_attribute<float>("VPosition");
1131 vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
1135 if (*last_valign != valign) {
1136 mismatched_valign = true;
1139 last_valign = valign;
1141 if (!mismatched_valign) {
1143 if (*last_valign == "top" || *last_valign == "center") {
1144 if (*vpos < *last_vpos) {
1145 incorrect_order = true;
1148 if (*vpos > *last_vpos) {
1149 incorrect_order = true;
1158 for (auto i: node->node_children()) {
1163 for (auto reel: reels) {
1164 for (auto ccap: reel->closed_captions()) {
1165 auto reel_xml = ccap->asset()->raw_xml();
1167 context.warning(VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED);
1171 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1172 * read in by libdcp's parser.
1175 shared_ptr<cxml::Document> doc;
1177 optional<Time> start_time;
1179 doc = make_shared<cxml::Document>("SubtitleReel");
1180 doc->read_string (*reel_xml);
1182 doc = make_shared<cxml::Document>("DCSubtitle");
1183 doc->read_string (*reel_xml);
1189 if (mismatched_valign) {
1190 context.error(VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN);
1193 if (incorrect_order) {
1194 context.error(VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING);
1199 struct LinesCharactersResult
1201 bool warning_length_exceeded = false;
1202 bool error_length_exceeded = false;
1203 bool line_count_exceeded = false;
1209 verify_text_lines_and_characters (
1210 shared_ptr<SubtitleAsset> asset,
1213 LinesCharactersResult* result
1219 Event (Time time_, float position_, int characters_)
1221 , position (position_)
1222 , characters (characters_)
1225 Event (Time time_, shared_ptr<Event> start_)
1231 int position; //< position from 0 at top of screen to 100 at bottom
1233 shared_ptr<Event> start;
1236 vector<shared_ptr<Event>> events;
1238 auto position = [](shared_ptr<const SubtitleString> sub) {
1239 switch (sub->v_align()) {
1241 return lrintf(sub->v_position() * 100);
1242 case VAlign::CENTER:
1243 return lrintf((0.5f + sub->v_position()) * 100);
1244 case VAlign::BOTTOM:
1245 return lrintf((1.0f - sub->v_position()) * 100);
1251 for (auto j: asset->subtitles()) {
1252 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1254 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1255 events.push_back(in);
1256 events.push_back(make_shared<Event>(text->out(), in));
1260 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1261 return a->time < b->time;
1264 map<int, int> current;
1265 for (auto i: events) {
1266 if (current.size() > 3) {
1267 result->line_count_exceeded = true;
1269 for (auto j: current) {
1270 if (j.second > warning_length) {
1271 result->warning_length_exceeded = true;
1273 if (j.second > error_length) {
1274 result->error_length_exceeded = true;
1279 /* end of a subtitle */
1280 DCP_ASSERT (current.find(i->start->position) != current.end());
1281 if (current[i->start->position] == i->start->characters) {
1282 current.erase(i->start->position);
1284 current[i->start->position] -= i->start->characters;
1287 /* start of a subtitle */
1288 if (current.find(i->position) == current.end()) {
1289 current[i->position] = i->characters;
1291 current[i->position] += i->characters;
1300 verify_text_details(Context& context, vector<shared_ptr<Reel>> reels)
1302 if (reels.empty()) {
1306 if (reels[0]->main_subtitle() && reels[0]->main_subtitle()->asset_ref().resolved()) {
1307 verify_text_details(context, reels, reels[0]->main_subtitle()->edit_rate().numerator,
1308 [](shared_ptr<Reel> reel) {
1309 return static_cast<bool>(reel->main_subtitle());
1311 [](shared_ptr<Reel> reel) {
1312 return reel->main_subtitle()->asset()->raw_xml();
1314 [](shared_ptr<Reel> reel) {
1315 return reel->main_subtitle()->actual_duration();
1317 [](shared_ptr<Reel> reel) {
1318 return reel->main_subtitle()->id();
1323 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1324 verify_text_details(context, reels, reels[0]->closed_captions()[i]->edit_rate().numerator,
1325 [i](shared_ptr<Reel> reel) {
1326 return i < reel->closed_captions().size();
1328 [i](shared_ptr<Reel> reel) {
1329 return reel->closed_captions()[i]->asset()->raw_xml();
1331 [i](shared_ptr<Reel> reel) {
1332 return reel->closed_captions()[i]->actual_duration();
1334 [i](shared_ptr<Reel> reel) {
1335 return reel->closed_captions()[i]->id();
1340 verify_closed_caption_details(context, reels);
1345 verify_extension_metadata(Context& context, shared_ptr<const CPL> cpl)
1347 DCP_ASSERT (cpl->file());
1348 cxml::Document doc ("CompositionPlaylist");
1349 doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1351 auto missing = false;
1354 if (auto reel_list = doc.node_child("ReelList")) {
1355 auto reels = reel_list->node_children("Reel");
1356 if (!reels.empty()) {
1357 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1358 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1359 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1361 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1362 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1366 if (auto name = extension->optional_node_child("Name")) {
1367 if (name->content() != "Application") {
1368 malformed = "<Name> should be 'Application'";
1371 if (auto property_list = extension->optional_node_child("PropertyList")) {
1372 if (auto property = property_list->optional_node_child("Property")) {
1373 if (auto name = property->optional_node_child("Name")) {
1374 if (name->content() != "DCP Constraints Profile") {
1375 malformed = "<Name> property should be 'DCP Constraints Profile'";
1378 if (auto value = property->optional_node_child("Value")) {
1379 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1380 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1395 context.bv21_error(VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->file().get());
1396 } else if (!malformed.empty()) {
1397 context.bv21_error(VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get());
1403 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1405 vector<string> encrypted;
1406 for (auto i: dcp->cpls()) {
1407 for (auto j: i->reel_file_assets()) {
1408 if (j->asset_ref().resolved()) {
1409 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1410 if (mxf && mxf->encrypted()) {
1411 encrypted.push_back(j->asset_ref().id());
1417 for (auto i: pkl->assets()) {
1418 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1431 shared_ptr<const Reel> reel,
1432 int64_t start_frame,
1433 optional<dcp::Size> main_picture_active_area,
1434 bool* have_main_subtitle,
1435 bool* have_no_main_subtitle,
1436 size_t* most_closed_captions,
1437 size_t* fewest_closed_captions,
1438 map<Marker, Time>* markers_seen
1441 for (auto i: reel->assets()) {
1442 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1443 context.error(VerificationNote::Code::INVALID_DURATION, i->id());
1445 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1446 context.error(VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id());
1448 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1449 if (i->encryptable() && !file_asset->hash()) {
1450 context.bv21_error(VerificationNote::Code::MISSING_HASH, i->id());
1454 if (context.dcp->standard() == Standard::SMPTE) {
1455 boost::optional<int64_t> duration;
1456 for (auto i: reel->assets()) {
1458 duration = i->actual_duration();
1459 } else if (*duration != i->actual_duration()) {
1460 context.bv21_error(VerificationNote::Code::MISMATCHED_ASSET_DURATION);
1466 if (reel->main_picture()) {
1467 /* Check reel stuff */
1468 auto const frame_rate = reel->main_picture()->frame_rate();
1469 if (frame_rate.denominator != 1 ||
1470 (frame_rate.numerator != 24 &&
1471 frame_rate.numerator != 25 &&
1472 frame_rate.numerator != 30 &&
1473 frame_rate.numerator != 48 &&
1474 frame_rate.numerator != 50 &&
1475 frame_rate.numerator != 60 &&
1476 frame_rate.numerator != 96)) {
1477 context.error(VerificationNote::Code::INVALID_PICTURE_FRAME_RATE, String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator));
1480 if (reel->main_picture()->asset_ref().resolved()) {
1481 verify_main_picture_asset(context, reel->main_picture(), start_frame);
1482 auto const asset_size = reel->main_picture()->asset()->size();
1483 if (main_picture_active_area) {
1484 if (main_picture_active_area->width > asset_size.width) {
1486 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1487 String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1488 context.cpl->file().get()
1491 if (main_picture_active_area->height > asset_size.height) {
1493 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1494 String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1495 context.cpl->file().get()
1503 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1504 verify_main_sound_asset(context, reel->main_sound());
1507 if (reel->main_subtitle()) {
1508 verify_main_subtitle_reel(context, reel->main_subtitle());
1509 if (reel->main_subtitle()->asset_ref().resolved()) {
1510 verify_subtitle_asset(context, reel->main_subtitle()->asset(), reel->main_subtitle()->duration());
1512 *have_main_subtitle = true;
1514 *have_no_main_subtitle = true;
1517 for (auto i: reel->closed_captions()) {
1518 verify_closed_caption_reel(context, i);
1519 if (i->asset_ref().resolved()) {
1520 verify_closed_caption_asset(context, i->asset(), i->duration());
1524 if (reel->main_markers()) {
1525 for (auto const& i: reel->main_markers()->get()) {
1526 markers_seen->insert(i);
1528 if (reel->main_markers()->entry_point()) {
1529 context.error(VerificationNote::Code::UNEXPECTED_ENTRY_POINT);
1531 if (reel->main_markers()->duration()) {
1532 context.error(VerificationNote::Code::UNEXPECTED_DURATION);
1536 *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1537 *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1544 verify_cpl(Context& context, shared_ptr<const CPL> cpl)
1546 context.stage("Checking CPL", cpl->file());
1547 validate_xml(context, cpl->file().get());
1549 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1550 context.bv21_error(VerificationNote::Code::PARTIALLY_ENCRYPTED);
1551 } else if (cpl->all_encrypted()) {
1552 context.ok(VerificationNote::Code::ALL_ENCRYPTED);
1553 } else if (!cpl->all_encrypted()) {
1554 context.ok(VerificationNote::Code::NONE_ENCRYPTED);
1557 for (auto const& i: cpl->additional_subtitle_languages()) {
1558 verify_language_tag(context, i);
1561 if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1562 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1563 * of the approved ones.
1565 auto all = ContentKind::all();
1566 auto name = cpl->content_kind().name();
1567 transform(name.begin(), name.end(), name.begin(), ::tolower);
1568 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1569 if (iter == all.end()) {
1570 context.error(VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name());
1574 if (cpl->release_territory()) {
1575 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") {
1576 auto terr = cpl->release_territory().get();
1577 /* Must be a valid region tag, or "001" */
1579 LanguageTag::RegionSubtag test(terr);
1581 if (terr != "001") {
1582 context.bv21_error(VerificationNote::Code::INVALID_LANGUAGE, terr);
1588 for (auto version: cpl->content_versions()) {
1589 if (version.label_text.empty()) {
1590 context.warning(VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT, cpl->file().get());
1595 if (context.dcp->standard() == Standard::SMPTE) {
1596 if (!cpl->annotation_text()) {
1597 context.bv21_error(VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->file().get());
1598 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1599 context.warning(VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->file().get());
1603 for (auto i: context.dcp->pkls()) {
1604 /* Check that the CPL's hash corresponds to the PKL */
1605 optional<string> h = i->hash(cpl->id());
1606 auto calculated_cpl_hash = make_digest(ArrayData(*cpl->file()));
1607 if (h && calculated_cpl_hash != *h) {
1609 dcp::VerificationNote(
1610 VerificationNote::Type::ERROR,
1611 VerificationNote::Code::MISMATCHED_CPL_HASHES,
1613 ).set_calculated_hash(calculated_cpl_hash).set_reference_hash(*h)
1616 context.ok(VerificationNote::Code::MATCHING_CPL_HASHES);
1619 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1620 optional<string> required_annotation_text;
1621 for (auto j: i->assets()) {
1622 /* See if this is a CPL */
1623 for (auto k: context.dcp->cpls()) {
1624 if (j->id() == k->id()) {
1625 if (!required_annotation_text) {
1626 /* First CPL we have found; this is the required AnnotationText unless we find another */
1627 required_annotation_text = cpl->content_title_text();
1629 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1630 required_annotation_text = boost::none;
1636 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1637 context.bv21_error(VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get());
1641 /* set to true if any reel has a MainSubtitle */
1642 auto have_main_subtitle = false;
1643 /* set to true if any reel has no MainSubtitle */
1644 auto have_no_main_subtitle = false;
1645 /* fewest number of closed caption assets seen in a reel */
1646 size_t fewest_closed_captions = SIZE_MAX;
1647 /* most number of closed caption assets seen in a reel */
1648 size_t most_closed_captions = 0;
1649 map<Marker, Time> markers_seen;
1651 auto const main_picture_active_area = cpl->main_picture_active_area();
1652 if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1654 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1655 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1659 if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1661 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1662 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1668 for (auto reel: cpl->reels()) {
1669 context.stage("Checking reel", optional<boost::filesystem::path>());
1674 main_picture_active_area,
1675 &have_main_subtitle,
1676 &have_no_main_subtitle,
1677 &most_closed_captions,
1678 &fewest_closed_captions,
1681 frame += reel->duration();
1684 verify_text_details(context, cpl->reels());
1686 if (context.dcp->standard() == Standard::SMPTE) {
1687 if (auto msc = cpl->main_sound_configuration()) {
1688 if (context.audio_channels && msc->channels() != *context.audio_channels) {
1690 VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION,
1691 String::compose("MainSoundConfiguration has %1 channels but sound assets have %2", msc->channels(), *context.audio_channels),
1697 if (have_main_subtitle && have_no_main_subtitle) {
1698 context.bv21_error(VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS);
1701 if (fewest_closed_captions != most_closed_captions) {
1702 context.bv21_error(VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS);
1705 if (cpl->content_kind() == ContentKind::FEATURE) {
1706 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1707 context.bv21_error(VerificationNote::Code::MISSING_FFEC_IN_FEATURE);
1709 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1710 context.bv21_error(VerificationNote::Code::MISSING_FFMC_IN_FEATURE);
1714 auto ffoc = markers_seen.find(Marker::FFOC);
1715 if (ffoc == markers_seen.end()) {
1716 context.warning(VerificationNote::Code::MISSING_FFOC);
1717 } else if (ffoc->second.e != 1) {
1718 context.warning(VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e));
1721 auto lfoc = markers_seen.find(Marker::LFOC);
1722 if (lfoc == markers_seen.end()) {
1723 context.warning(VerificationNote::Code::MISSING_LFOC);
1725 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1726 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1727 context.warning(VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time));
1731 LinesCharactersResult result;
1732 for (auto reel: cpl->reels()) {
1733 if (reel->main_subtitle() && reel->main_subtitle()->asset_ref().resolved()) {
1734 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1738 if (result.line_count_exceeded) {
1739 context.warning(VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT);
1741 if (result.error_length_exceeded) {
1742 context.warning(VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH);
1743 } else if (result.warning_length_exceeded) {
1744 context.warning(VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH);
1747 result = LinesCharactersResult();
1748 for (auto reel: cpl->reels()) {
1749 for (auto i: reel->closed_captions()) {
1751 verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1756 if (result.line_count_exceeded) {
1757 context.bv21_error(VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT);
1759 if (result.error_length_exceeded) {
1760 context.bv21_error(VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH);
1763 if (!cpl->read_composition_metadata()) {
1764 context.bv21_error(VerificationNote::Code::MISSING_CPL_METADATA, cpl->file().get());
1765 } else if (!cpl->version_number()) {
1766 context.bv21_error(VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->file().get());
1769 verify_extension_metadata(context, cpl);
1771 if (cpl->any_encrypted()) {
1772 cxml::Document doc("CompositionPlaylist");
1773 DCP_ASSERT(cpl->file());
1774 doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1775 if (!doc.optional_node_child("Signature")) {
1776 context.bv21_error(VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->file().get());
1785 verify_pkl(Context& context, shared_ptr<const PKL> pkl)
1787 validate_xml(context, pkl->file().get());
1789 if (pkl_has_encrypted_assets(context.dcp, pkl)) {
1790 cxml::Document doc("PackingList");
1791 doc.read_file(dcp::filesystem::fix_long_path(pkl->file().get()));
1792 if (!doc.optional_node_child("Signature")) {
1793 context.bv21_error(VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get());
1797 set<string> uuid_set;
1798 for (auto asset: pkl->assets()) {
1799 if (!uuid_set.insert(asset->id()).second) {
1800 context.error(VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get());
1810 verify_assetmap(Context& context, shared_ptr<const DCP> dcp)
1812 auto asset_map = dcp->asset_map();
1813 DCP_ASSERT(asset_map);
1815 validate_xml(context, asset_map->file().get());
1817 set<string> uuid_set;
1818 for (auto const& asset: asset_map->assets()) {
1819 if (!uuid_set.insert(asset.id()).second) {
1820 context.error(VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get());
1829 vector<boost::filesystem::path> directories,
1830 vector<dcp::DecryptedKDM> kdms,
1831 function<void (string, optional<boost::filesystem::path>)> stage,
1832 function<void (float)> progress,
1833 VerificationOptions options,
1834 optional<boost::filesystem::path> xsd_dtd_directory
1837 if (!xsd_dtd_directory) {
1838 xsd_dtd_directory = resources_directory() / "xsd";
1840 *xsd_dtd_directory = filesystem::canonical(*xsd_dtd_directory);
1842 vector<VerificationNote> notes;
1843 Context context(notes, *xsd_dtd_directory, stage, progress, options);
1845 vector<shared_ptr<DCP>> dcps;
1846 for (auto i: directories) {
1847 dcps.push_back (make_shared<DCP>(i));
1850 for (auto dcp: dcps) {
1851 stage ("Checking DCP", dcp->directory());
1855 bool carry_on = true;
1857 dcp->read (¬es, true);
1858 } catch (MissingAssetmapError& e) {
1859 context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1861 } catch (ReadError& e) {
1862 context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1863 } catch (XMLError& e) {
1864 context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1865 } catch (MXFFileError& e) {
1866 context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1867 } catch (BadURNUUIDError& e) {
1868 context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1869 } catch (cxml::Error& e) {
1870 context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1877 if (dcp->standard() != Standard::SMPTE) {
1878 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1881 for (auto kdm: kdms) {
1885 for (auto cpl: dcp->cpls()) {
1888 verify_cpl(context, cpl);
1889 context.cpl.reset();
1890 } catch (ReadError& e) {
1891 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1895 for (auto pkl: dcp->pkls()) {
1896 stage("Checking PKL", pkl->file());
1897 verify_pkl(context, pkl);
1900 if (dcp->asset_map_file()) {
1901 stage("Checking ASSETMAP", dcp->asset_map_file().get());
1902 verify_assetmap(context, dcp);
1904 context.error(VerificationNote::Code::MISSING_ASSETMAP);
1908 return { notes, dcps };
1913 dcp::note_to_string (VerificationNote note)
1915 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1917 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1918 * not "ClosedCaption assets must have an <EntryPoint> tag."
1920 * It's OK to use XML tag names where they are clear.
1921 * If both ID and filename are available, use only the ID.
1922 * End messages with a full stop.
1923 * Messages should not mention whether or not their errors are a part of Bv2.1.
1925 switch (note.code()) {
1926 case VerificationNote::Code::FAILED_READ:
1927 return *note.note();
1928 case VerificationNote::Code::MATCHING_CPL_HASHES:
1929 return "The hash of the CPL in the PKL matches the CPL file.";
1930 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1931 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());
1932 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1933 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1934 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1935 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());
1936 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1937 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1938 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1939 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());
1940 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1941 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1942 case VerificationNote::Code::EMPTY_ASSET_PATH:
1943 return "The asset map contains an empty asset path.";
1944 case VerificationNote::Code::MISSING_ASSET:
1945 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1946 case VerificationNote::Code::MISMATCHED_STANDARD:
1947 return "The DCP contains both SMPTE and Interop parts.";
1948 case VerificationNote::Code::INVALID_XML:
1949 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1950 case VerificationNote::Code::MISSING_ASSETMAP:
1951 return "No valid ASSETMAP or ASSETMAP.xml was found.";
1952 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1953 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1954 case VerificationNote::Code::INVALID_DURATION:
1955 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1956 case VerificationNote::Code::VALID_PICTURE_FRAME_SIZES_IN_BYTES:
1957 return String::compose("Each frame of the picture asset %1 has a bit rate safely under the limit of 250Mbit/s.", note.file()->filename());
1958 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1959 return String::compose(
1960 "Frame %1 (timecode %2) in asset %3 has an instantaneous bit rate that is larger than the limit of 250Mbit/s.",
1962 dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
1963 note.file()->filename()
1965 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1966 return String::compose(
1967 "Frame %1 (timecode %2) in asset %3 has an instantaneous bit rate that is close to the limit of 250Mbit/s.",
1969 dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
1970 note.file()->filename()
1972 case VerificationNote::Code::EXTERNAL_ASSET:
1973 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());
1974 case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1975 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1976 case VerificationNote::Code::INVALID_STANDARD:
1977 return "This DCP does not use the SMPTE standard.";
1978 case VerificationNote::Code::INVALID_LANGUAGE:
1979 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1980 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1981 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1982 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1983 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1984 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1985 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1986 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1987 return "3D 4K DCPs are not allowed.";
1988 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1989 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1990 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1991 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1992 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1993 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());
1994 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1995 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1996 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1997 return "Some subtitle assets have different <Language> tags than others";
1998 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1999 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
2000 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
2001 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
2002 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
2003 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
2004 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
2005 return "At least one subtitle lasts less than 15 frames.";
2006 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
2007 return "At least one pair of subtitles is separated by less than 2 frames.";
2008 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
2009 return "At least one subtitle extends outside of its reel.";
2010 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
2011 return "There are more than 3 subtitle lines in at least one place in the DCP.";
2012 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
2013 return "There are more than 52 characters in at least one subtitle line.";
2014 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
2015 return "There are more than 79 characters in at least one subtitle line.";
2016 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
2017 return "There are more than 3 closed caption lines in at least one place.";
2018 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
2019 return "There are more than 32 characters in at least one closed caption line.";
2020 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
2021 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
2022 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
2023 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.cpl_id().get());
2024 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
2025 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.cpl_id().get());
2026 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
2027 return "All assets in a reel do not have the same duration.";
2028 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
2029 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
2030 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
2031 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
2032 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
2033 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
2034 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
2035 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
2036 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
2037 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
2038 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
2039 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
2040 case VerificationNote::Code::MISSING_HASH:
2041 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
2042 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
2043 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
2044 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
2045 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
2046 case VerificationNote::Code::MISSING_FFOC:
2047 return "There should be a FFOC (first frame of content) marker.";
2048 case VerificationNote::Code::MISSING_LFOC:
2049 return "There should be a LFOC (last frame of content) marker.";
2050 case VerificationNote::Code::INCORRECT_FFOC:
2051 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
2052 case VerificationNote::Code::INCORRECT_LFOC:
2053 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
2054 case VerificationNote::Code::MISSING_CPL_METADATA:
2055 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.cpl_id().get());
2056 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
2057 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.cpl_id().get());
2058 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
2059 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.cpl_id().get());
2060 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
2061 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
2062 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
2063 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.cpl_id().get());
2064 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
2065 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
2066 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
2067 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
2068 case VerificationNote::Code::ALL_ENCRYPTED:
2069 return "All the assets are encrypted.";
2070 case VerificationNote::Code::NONE_ENCRYPTED:
2071 return "All the assets are unencrypted.";
2072 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
2073 return "Some assets are encrypted but some are not.";
2074 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
2075 return String::compose(
2076 "Frame %1 (timecode %2) has an invalid JPEG2000 codestream (%3).",
2078 dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
2081 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
2082 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
2083 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
2084 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
2085 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
2086 return "The JPEG2000 tile size is not the same as the image size.";
2087 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
2088 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
2089 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
2090 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
2091 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
2092 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
2093 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
2094 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
2095 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
2096 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
2097 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
2098 return "POC marker found outside main header.";
2099 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
2100 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
2101 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
2102 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
2103 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
2104 return "No TLM marker was found in a JPEG2000 codestream.";
2105 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
2106 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
2107 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
2108 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
2109 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
2111 vector<string> parts;
2112 boost::split (parts, note.note().get(), boost::is_any_of(" "));
2113 DCP_ASSERT (parts.size() == 2);
2114 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]);
2116 case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
2117 return "Some aspect of this DCP could not be checked because it is encrypted.";
2118 case VerificationNote::Code::EMPTY_TEXT:
2119 return "There is an empty <Text> node in a subtitle or closed caption.";
2120 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
2121 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
2122 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
2123 return "Some closed captions are not listed in the order of their vertical position.";
2124 case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
2125 return "There is an <EntryPoint> node inside a <MainMarkers>.";
2126 case VerificationNote::Code::UNEXPECTED_DURATION:
2127 return "There is an <Duration> node inside a <MainMarkers>.";
2128 case VerificationNote::Code::INVALID_CONTENT_KIND:
2129 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
2130 case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
2131 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
2132 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
2133 return String::compose("The PKL %1 has more than one asset with the same ID.", note.note().get());
2134 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
2135 return String::compose("The ASSETMAP %1 has more than one asset with the same ID.", note.note().get());
2136 case VerificationNote::Code::MISSING_SUBTITLE:
2137 return String::compose("The subtitle asset %1 has no subtitles.", note.note().get());
2138 case VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
2139 return String::compose("<IssueDate> has an invalid value: %1", note.note().get());
2140 case VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS:
2141 return String::compose("The sound assets do not all have the same channel count; the first to differ is %1", note.file()->filename());
2142 case VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION:
2143 return String::compose("<MainSoundConfiguration> has an invalid value: %1", note.note().get());
2144 case VerificationNote::Code::MISSING_FONT:
2145 return String::compose("The font file for font ID \"%1\" was not found, or was not referred to in the ASSETMAP.", note.note().get());
2146 case VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE:
2147 return String::compose(
2148 "Frame %1 has an image component that is too large (component %2 is %3 bytes in size).",
2149 note.frame().get(), note.component().get(), note.size().get()
2151 case VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT:
2152 return String::compose("The XML in the subtitle asset %1 has more than one namespace declaration.", note.note().get());
2153 case VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT:
2154 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());
2155 case VerificationNote::Code::MISSING_LOAD_FONT:
2156 return String::compose("The SMPTE subtitle asset %1 has <Text> nodes but no <LoadFont> node", note.id().get());
2157 case VerificationNote::Code::MISMATCHED_ASSET_MAP_ID:
2158 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());
2159 case VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT:
2160 return String::compose("The <LabelText> in a <ContentVersion> in CPL %1 is empty", note.cpl_id().get());
2168 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2170 return a.type() == b.type() &&
2171 a.code() == b.code() &&
2172 a.note() == b.note() &&
2173 a.file() == b.file() &&
2174 a.line() == b.line() &&
2175 a.frame() == b.frame() &&
2176 a.component() == b.component() &&
2177 a.size() == b.size() &&
2179 a.other_id() == b.other_id() &&
2180 a.frame_rate() == b.frame_rate() &&
2181 a.cpl_id() == b.cpl_id() &&
2182 a.reference_hash() == b.reference_hash() &&
2183 a.calculated_hash() == b.calculated_hash();
2188 dcp::operator!=(dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2195 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2197 if (a.type() != b.type()) {
2198 return a.type() < b.type();
2201 if (a.code() != b.code()) {
2202 return a.code() < b.code();
2205 if (a.note() != b.note()) {
2206 return a.note().get_value_or("") < b.note().get_value_or("");
2209 if (a.file() != b.file()) {
2210 return a.file().get_value_or("") < b.file().get_value_or("");
2213 if (a.line() != b.line()) {
2214 return a.line().get_value_or(0) < b.line().get_value_or(0);
2217 if (a.frame() != b.frame()) {
2218 return a.frame().get_value_or(0) < b.frame().get_value_or(0);
2221 if (a.component() != b.component()) {
2222 return a.component().get_value_or(0) < b.component().get_value_or(0);
2225 if (a.size() != b.size()) {
2226 return a.size().get_value_or(0) < b.size().get_value_or(0);
2229 if (a.id() != b.id()) {
2230 return a.id().get_value_or("") < b.id().get_value_or("");
2233 if (a.other_id() != b.other_id()) {
2234 return a.other_id().get_value_or("") < b.other_id().get_value_or("");
2237 return a.frame_rate().get_value_or(0) != b.frame_rate().get_value_or(0);
2242 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2244 s << note_to_string (note);
2246 s << " [" << note.note().get() << "]";
2249 s << " [" << note.file().get() << "]";
2252 s << " [" << note.line().get() << "]";