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);
622 context.ok(VerificationNote::Code::CORRECT_PICTURE_HASH, file);
627 context.stage("Checking picture frame sizes", asset->file());
628 verify_picture_asset(context, reel_asset, file, start_frame);
630 /* Only flat/scope allowed by Bv2.1 */
632 asset->size() != Size(2048, 858) &&
633 asset->size() != Size(1998, 1080) &&
634 asset->size() != Size(4096, 1716) &&
635 asset->size() != Size(3996, 2160)) {
636 context.bv21_error(VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS, String::compose("%1x%2", asset->size().width, asset->size().height), file);
639 /* Only 24, 25, 48fps allowed for 2K */
641 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
642 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
645 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
646 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
651 if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
652 /* Only 24fps allowed for 4K */
653 if (asset->edit_rate() != Fraction(24, 1)) {
655 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
656 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
661 /* Only 2D allowed for 4K */
662 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
664 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
665 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
675 verify_main_sound_asset(Context& context, shared_ptr<const ReelSoundAsset> reel_asset)
677 auto asset = reel_asset->asset();
678 auto const file = *asset->file();
680 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)) {
681 context.stage("Checking sound asset hash", file);
682 string reference_hash;
683 string calculated_hash;
684 auto const r = verify_asset(context, reel_asset, &reference_hash, &calculated_hash);
686 case VerifyAssetResult::BAD:
688 dcp::VerificationNote(
689 VerificationNote::Type::ERROR,
690 VerificationNote::Code::INCORRECT_SOUND_HASH,
692 ).set_reference_hash(reference_hash).set_calculated_hash(calculated_hash)
695 case VerifyAssetResult::CPL_PKL_DIFFER:
696 context.error(VerificationNote::Code::MISMATCHED_SOUND_HASHES, file);
703 if (!context.audio_channels) {
704 context.audio_channels = asset->channels();
705 } else if (*context.audio_channels != asset->channels()) {
706 context.error(VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS, file);
709 context.stage("Checking sound asset metadata", file);
711 if (auto lang = asset->language()) {
712 verify_language_tag(context, *lang);
714 if (asset->sampling_rate() != 48000) {
715 context.bv21_error(VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), file);
721 verify_main_subtitle_reel(Context& context, shared_ptr<const ReelSubtitleAsset> reel_asset)
723 /* XXX: is Language compulsory? */
724 if (reel_asset->language()) {
725 verify_language_tag(context, *reel_asset->language());
728 if (!reel_asset->entry_point()) {
729 context.bv21_error(VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id());
730 } else if (reel_asset->entry_point().get()) {
731 context.bv21_error(VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id());
737 verify_closed_caption_reel(Context& context, shared_ptr<const ReelClosedCaptionAsset> reel_asset)
739 /* XXX: is Language compulsory? */
740 if (reel_asset->language()) {
741 verify_language_tag(context, *reel_asset->language());
744 if (!reel_asset->entry_point()) {
745 context.bv21_error(VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id());
746 } else if (reel_asset->entry_point().get()) {
747 context.bv21_error(VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id());
752 /** Verify stuff that is common to both subtitles and closed captions */
754 verify_smpte_timed_text_asset (
756 shared_ptr<const SMPTESubtitleAsset> asset,
757 optional<int64_t> reel_asset_duration
760 if (asset->language()) {
761 verify_language_tag(context, *asset->language());
763 context.bv21_error(VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file());
766 auto const size = filesystem::file_size(asset->file().get());
767 if (size > 115 * 1024 * 1024) {
768 context.bv21_error(VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file());
771 /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
772 * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
774 auto fonts = asset->font_data ();
776 for (auto i: fonts) {
777 total_size += i.second.size();
779 if (total_size > 10 * 1024 * 1024) {
780 context.bv21_error(VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get());
783 if (!asset->start_time()) {
784 context.bv21_error(VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get());
785 } else if (asset->start_time() != Time()) {
786 context.bv21_error(VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get());
789 if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
791 VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
792 String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
799 /** Verify Interop subtitle / CCAP stuff */
801 verify_interop_text_asset(Context& context, shared_ptr<const InteropSubtitleAsset> asset)
803 if (asset->subtitles().empty()) {
804 context.error(VerificationNote::Code::MISSING_SUBTITLE, asset->id(), asset->file().get());
806 auto const unresolved = asset->unresolved_fonts();
807 if (!unresolved.empty()) {
808 context.error(VerificationNote::Code::MISSING_FONT, unresolved.front());
813 /** Verify SMPTE subtitle-only stuff */
815 verify_smpte_subtitle_asset(Context& context, shared_ptr<const SMPTESubtitleAsset> asset)
817 if (asset->language()) {
818 if (!context.subtitle_language) {
819 context.subtitle_language = *asset->language();
820 } else if (context.subtitle_language != *asset->language()) {
821 context.bv21_error(VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES);
825 DCP_ASSERT (asset->resource_id());
826 auto xml_id = asset->xml_id();
828 if (asset->resource_id().get() != xml_id) {
829 context.bv21_error(VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID);
832 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
833 context.bv21_error(VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID);
836 context.warning(VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED);
839 if (asset->raw_xml()) {
840 /* Deluxe require this in their QC even if it seems never to be mentioned in any standard */
841 cxml::Document doc("SubtitleReel");
842 doc.read_string(*asset->raw_xml());
843 auto issue_date = doc.string_child("IssueDate");
844 std::regex reg("^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d$");
845 if (!std::regex_match(issue_date, reg)) {
846 context.warning(VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, issue_date);
852 /** Verify all subtitle stuff */
854 verify_subtitle_asset(Context& context, shared_ptr<const SubtitleAsset> asset, optional<int64_t> reel_asset_duration)
856 context.stage("Checking subtitle XML", asset->file());
857 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
858 * gets passed through libdcp which may clean up and therefore hide errors.
860 if (asset->raw_xml()) {
861 validate_xml(context, asset->raw_xml().get());
863 context.warning(VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED);
866 auto namespace_count = [](shared_ptr<const SubtitleAsset> asset, string root_node) {
867 cxml::Document doc(root_node);
868 doc.read_string(asset->raw_xml().get());
869 auto root = dynamic_cast<xmlpp::Element*>(doc.node())->cobj();
871 for (auto ns = root->nsDef; ns != nullptr; ns = ns->next) {
877 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
879 verify_interop_text_asset(context, interop);
880 if (namespace_count(asset, "DCSubtitle") > 1) {
881 context.warning(VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id());
885 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
887 verify_smpte_timed_text_asset(context, smpte, reel_asset_duration);
888 verify_smpte_subtitle_asset(context, smpte);
889 /* This asset may be encrypted and in that case we'll have no raw_xml() */
890 if (asset->raw_xml() && namespace_count(asset, "SubtitleReel") > 1) {
891 context.warning(VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id());
897 /** Verify all closed caption stuff */
899 verify_closed_caption_asset (
901 shared_ptr<const SubtitleAsset> asset,
902 optional<int64_t> reel_asset_duration
905 context.stage("Checking closed caption XML", asset->file());
906 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
907 * gets passed through libdcp which may clean up and therefore hide errors.
909 auto raw_xml = asset->raw_xml();
911 validate_xml(context, *raw_xml);
912 if (raw_xml->size() > 256 * 1024) {
913 context.bv21_error(VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file());
916 context.warning(VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED);
919 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
921 verify_interop_text_asset(context, interop);
924 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
926 verify_smpte_timed_text_asset(context, smpte, reel_asset_duration);
931 /** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes etc. */
934 verify_text_details (
936 vector<shared_ptr<Reel>> reels,
938 std::function<bool (shared_ptr<Reel>)> check,
939 std::function<optional<string> (shared_ptr<Reel>)> xml,
940 std::function<int64_t (shared_ptr<Reel>)> duration,
941 std::function<std::string (shared_ptr<Reel>)> id
944 /* end of last subtitle (in editable units) */
945 optional<int64_t> last_out;
946 auto too_short = false;
947 auto too_close = false;
948 auto too_early = false;
949 auto reel_overlap = false;
950 auto empty_text = false;
951 /* current reel start time (in editable units) */
952 int64_t reel_offset = 0;
953 optional<string> missing_load_font_id;
955 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool, bool&, vector<string>&)> parse;
957 parse = [&parse, &last_out, &too_short, &too_close, &too_early, &empty_text, &reel_offset, &missing_load_font_id](
958 cxml::ConstNodePtr node,
960 optional<Time> start_time,
964 vector<string>& font_ids
966 if (node->name() == "Subtitle") {
967 Time in (node->string_attribute("TimeIn"), tcr);
971 Time out (node->string_attribute("TimeOut"), tcr);
975 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
978 auto length = out - in;
979 if (length.as_editable_units_ceil(er) < 15) {
983 /* XXX: this feels dubious - is it really what Bv2.1 means? */
984 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
985 if (distance >= 0 && distance < 2) {
989 last_out = reel_offset + out.as_editable_units_floor(er);
990 } else if (node->name() == "Text") {
991 std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
992 if (!node->content().empty()) {
995 for (auto i: node->node_children()) {
996 if (node_has_content(i)) {
1002 if (!node_has_content(node)) {
1006 } else if (node->name() == "LoadFont") {
1007 if (auto const id = node->optional_string_attribute("Id")) {
1008 font_ids.push_back(*id);
1009 } else if (auto const id = node->optional_string_attribute("ID")) {
1010 font_ids.push_back(*id);
1012 } else if (node->name() == "Font") {
1013 if (auto const font_id = node->optional_string_attribute("Id")) {
1014 if (std::find_if(font_ids.begin(), font_ids.end(), [font_id](string const& id) { return id == font_id; }) == font_ids.end()) {
1015 missing_load_font_id = font_id;
1019 for (auto i: node->node_children()) {
1020 parse(i, tcr, start_time, er, first_reel, has_text, font_ids);
1024 for (auto i = 0U; i < reels.size(); ++i) {
1025 if (!check(reels[i])) {
1029 auto reel_xml = xml(reels[i]);
1031 context.warning(VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED);
1035 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1036 * read in by libdcp's parser.
1039 shared_ptr<cxml::Document> doc;
1041 optional<Time> start_time;
1042 switch (context.dcp->standard().get_value_or(dcp::Standard::SMPTE)) {
1043 case dcp::Standard::INTEROP:
1044 doc = make_shared<cxml::Document>("DCSubtitle");
1045 doc->read_string (*reel_xml);
1047 case dcp::Standard::SMPTE:
1048 doc = make_shared<cxml::Document>("SubtitleReel");
1049 doc->read_string (*reel_xml);
1050 tcr = doc->number_child<int>("TimeCodeRate");
1051 if (auto start_time_string = doc->optional_string_child("StartTime")) {
1052 start_time = Time(*start_time_string, tcr);
1056 bool has_text = false;
1057 vector<string> font_ids;
1058 parse(doc, tcr, start_time, edit_rate, i == 0, has_text, font_ids);
1059 auto end = reel_offset + duration(reels[i]);
1060 if (last_out && *last_out > end) {
1061 reel_overlap = true;
1065 if (context.dcp->standard() && *context.dcp->standard() == dcp::Standard::SMPTE && has_text && font_ids.empty()) {
1066 context.add_note(dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT).set_id(id(reels[i])));
1070 if (last_out && *last_out > reel_offset) {
1071 reel_overlap = true;
1075 context.warning(VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME);
1079 context.warning(VerificationNote::Code::INVALID_SUBTITLE_DURATION);
1083 context.warning(VerificationNote::Code::INVALID_SUBTITLE_SPACING);
1087 context.error(VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY);
1091 context.warning(VerificationNote::Code::EMPTY_TEXT);
1094 if (missing_load_font_id) {
1095 context.add_note(dcp::VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT).set_id(*missing_load_font_id));
1102 verify_closed_caption_details(Context& context, vector<shared_ptr<Reel>> reels)
1104 std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
1105 find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
1106 for (auto i: node->node_children()) {
1107 if (i->name() == "Text") {
1108 text_or_image.push_back (i);
1110 find_text_or_image (i, text_or_image);
1115 auto mismatched_valign = false;
1116 auto incorrect_order = false;
1118 std::function<void (cxml::ConstNodePtr)> parse;
1119 parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
1120 if (node->name() == "Subtitle") {
1121 vector<cxml::ConstNodePtr> text_or_image;
1122 find_text_or_image (node, text_or_image);
1123 optional<string> last_valign;
1124 optional<float> last_vpos;
1125 for (auto i: text_or_image) {
1126 auto valign = i->optional_string_attribute("VAlign");
1128 valign = i->optional_string_attribute("Valign").get_value_or("center");
1130 auto vpos = i->optional_number_attribute<float>("VPosition");
1132 vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
1136 if (*last_valign != valign) {
1137 mismatched_valign = true;
1140 last_valign = valign;
1142 if (!mismatched_valign) {
1144 if (*last_valign == "top" || *last_valign == "center") {
1145 if (*vpos < *last_vpos) {
1146 incorrect_order = true;
1149 if (*vpos > *last_vpos) {
1150 incorrect_order = true;
1159 for (auto i: node->node_children()) {
1164 for (auto reel: reels) {
1165 for (auto ccap: reel->closed_captions()) {
1166 auto reel_xml = ccap->asset()->raw_xml();
1168 context.warning(VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED);
1172 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1173 * read in by libdcp's parser.
1176 shared_ptr<cxml::Document> doc;
1178 optional<Time> start_time;
1180 doc = make_shared<cxml::Document>("SubtitleReel");
1181 doc->read_string (*reel_xml);
1183 doc = make_shared<cxml::Document>("DCSubtitle");
1184 doc->read_string (*reel_xml);
1190 if (mismatched_valign) {
1191 context.error(VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN);
1194 if (incorrect_order) {
1195 context.error(VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING);
1200 struct LinesCharactersResult
1202 bool warning_length_exceeded = false;
1203 bool error_length_exceeded = false;
1204 bool line_count_exceeded = false;
1210 verify_text_lines_and_characters (
1211 shared_ptr<SubtitleAsset> asset,
1214 LinesCharactersResult* result
1220 Event (Time time_, float position_, int characters_)
1222 , position (position_)
1223 , characters (characters_)
1226 Event (Time time_, shared_ptr<Event> start_)
1232 int position; //< position from 0 at top of screen to 100 at bottom
1234 shared_ptr<Event> start;
1237 vector<shared_ptr<Event>> events;
1239 auto position = [](shared_ptr<const SubtitleString> sub) {
1240 switch (sub->v_align()) {
1242 return lrintf(sub->v_position() * 100);
1243 case VAlign::CENTER:
1244 return lrintf((0.5f + sub->v_position()) * 100);
1245 case VAlign::BOTTOM:
1246 return lrintf((1.0f - sub->v_position()) * 100);
1252 for (auto j: asset->subtitles()) {
1253 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1255 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1256 events.push_back(in);
1257 events.push_back(make_shared<Event>(text->out(), in));
1261 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1262 return a->time < b->time;
1265 map<int, int> current;
1266 for (auto i: events) {
1267 if (current.size() > 3) {
1268 result->line_count_exceeded = true;
1270 for (auto j: current) {
1271 if (j.second > warning_length) {
1272 result->warning_length_exceeded = true;
1274 if (j.second > error_length) {
1275 result->error_length_exceeded = true;
1280 /* end of a subtitle */
1281 DCP_ASSERT (current.find(i->start->position) != current.end());
1282 if (current[i->start->position] == i->start->characters) {
1283 current.erase(i->start->position);
1285 current[i->start->position] -= i->start->characters;
1288 /* start of a subtitle */
1289 if (current.find(i->position) == current.end()) {
1290 current[i->position] = i->characters;
1292 current[i->position] += i->characters;
1301 verify_text_details(Context& context, vector<shared_ptr<Reel>> reels)
1303 if (reels.empty()) {
1307 if (reels[0]->main_subtitle() && reels[0]->main_subtitle()->asset_ref().resolved()) {
1308 verify_text_details(context, reels, reels[0]->main_subtitle()->edit_rate().numerator,
1309 [](shared_ptr<Reel> reel) {
1310 return static_cast<bool>(reel->main_subtitle());
1312 [](shared_ptr<Reel> reel) {
1313 return reel->main_subtitle()->asset()->raw_xml();
1315 [](shared_ptr<Reel> reel) {
1316 return reel->main_subtitle()->actual_duration();
1318 [](shared_ptr<Reel> reel) {
1319 return reel->main_subtitle()->id();
1324 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1325 verify_text_details(context, reels, reels[0]->closed_captions()[i]->edit_rate().numerator,
1326 [i](shared_ptr<Reel> reel) {
1327 return i < reel->closed_captions().size();
1329 [i](shared_ptr<Reel> reel) {
1330 return reel->closed_captions()[i]->asset()->raw_xml();
1332 [i](shared_ptr<Reel> reel) {
1333 return reel->closed_captions()[i]->actual_duration();
1335 [i](shared_ptr<Reel> reel) {
1336 return reel->closed_captions()[i]->id();
1341 verify_closed_caption_details(context, reels);
1346 verify_extension_metadata(Context& context, shared_ptr<const CPL> cpl)
1348 DCP_ASSERT (cpl->file());
1349 cxml::Document doc ("CompositionPlaylist");
1350 doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1352 auto missing = false;
1355 if (auto reel_list = doc.node_child("ReelList")) {
1356 auto reels = reel_list->node_children("Reel");
1357 if (!reels.empty()) {
1358 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1359 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1360 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1362 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1363 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1367 if (auto name = extension->optional_node_child("Name")) {
1368 if (name->content() != "Application") {
1369 malformed = "<Name> should be 'Application'";
1372 if (auto property_list = extension->optional_node_child("PropertyList")) {
1373 if (auto property = property_list->optional_node_child("Property")) {
1374 if (auto name = property->optional_node_child("Name")) {
1375 if (name->content() != "DCP Constraints Profile") {
1376 malformed = "<Name> property should be 'DCP Constraints Profile'";
1379 if (auto value = property->optional_node_child("Value")) {
1380 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1381 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1396 context.bv21_error(VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->file().get());
1397 } else if (!malformed.empty()) {
1398 context.bv21_error(VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get());
1404 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1406 vector<string> encrypted;
1407 for (auto i: dcp->cpls()) {
1408 for (auto j: i->reel_file_assets()) {
1409 if (j->asset_ref().resolved()) {
1410 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1411 if (mxf && mxf->encrypted()) {
1412 encrypted.push_back(j->asset_ref().id());
1418 for (auto i: pkl->assets()) {
1419 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1432 shared_ptr<const Reel> reel,
1433 int64_t start_frame,
1434 optional<dcp::Size> main_picture_active_area,
1435 bool* have_main_subtitle,
1436 bool* have_no_main_subtitle,
1437 size_t* most_closed_captions,
1438 size_t* fewest_closed_captions,
1439 map<Marker, Time>* markers_seen
1442 for (auto i: reel->assets()) {
1443 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1444 context.error(VerificationNote::Code::INVALID_DURATION, i->id());
1446 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1447 context.error(VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id());
1449 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1450 if (i->encryptable() && !file_asset->hash()) {
1451 context.bv21_error(VerificationNote::Code::MISSING_HASH, i->id());
1455 if (context.dcp->standard() == Standard::SMPTE) {
1456 boost::optional<int64_t> duration;
1457 for (auto i: reel->assets()) {
1459 duration = i->actual_duration();
1460 } else if (*duration != i->actual_duration()) {
1461 context.bv21_error(VerificationNote::Code::MISMATCHED_ASSET_DURATION);
1467 if (reel->main_picture()) {
1468 /* Check reel stuff */
1469 auto const frame_rate = reel->main_picture()->frame_rate();
1470 if (frame_rate.denominator != 1 ||
1471 (frame_rate.numerator != 24 &&
1472 frame_rate.numerator != 25 &&
1473 frame_rate.numerator != 30 &&
1474 frame_rate.numerator != 48 &&
1475 frame_rate.numerator != 50 &&
1476 frame_rate.numerator != 60 &&
1477 frame_rate.numerator != 96)) {
1478 context.error(VerificationNote::Code::INVALID_PICTURE_FRAME_RATE, String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator));
1481 if (reel->main_picture()->asset_ref().resolved()) {
1482 verify_main_picture_asset(context, reel->main_picture(), start_frame);
1483 auto const asset_size = reel->main_picture()->asset()->size();
1484 if (main_picture_active_area) {
1485 if (main_picture_active_area->width > asset_size.width) {
1487 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1488 String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1489 context.cpl->file().get()
1492 if (main_picture_active_area->height > asset_size.height) {
1494 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1495 String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1496 context.cpl->file().get()
1504 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1505 verify_main_sound_asset(context, reel->main_sound());
1508 if (reel->main_subtitle()) {
1509 verify_main_subtitle_reel(context, reel->main_subtitle());
1510 if (reel->main_subtitle()->asset_ref().resolved()) {
1511 verify_subtitle_asset(context, reel->main_subtitle()->asset(), reel->main_subtitle()->duration());
1513 *have_main_subtitle = true;
1515 *have_no_main_subtitle = true;
1518 for (auto i: reel->closed_captions()) {
1519 verify_closed_caption_reel(context, i);
1520 if (i->asset_ref().resolved()) {
1521 verify_closed_caption_asset(context, i->asset(), i->duration());
1525 if (reel->main_markers()) {
1526 for (auto const& i: reel->main_markers()->get()) {
1527 markers_seen->insert(i);
1529 if (reel->main_markers()->entry_point()) {
1530 context.error(VerificationNote::Code::UNEXPECTED_ENTRY_POINT);
1532 if (reel->main_markers()->duration()) {
1533 context.error(VerificationNote::Code::UNEXPECTED_DURATION);
1537 *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1538 *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1545 verify_cpl(Context& context, shared_ptr<const CPL> cpl)
1547 context.stage("Checking CPL", cpl->file());
1548 validate_xml(context, cpl->file().get());
1550 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1551 context.bv21_error(VerificationNote::Code::PARTIALLY_ENCRYPTED);
1552 } else if (cpl->all_encrypted()) {
1553 context.ok(VerificationNote::Code::ALL_ENCRYPTED);
1554 } else if (!cpl->all_encrypted()) {
1555 context.ok(VerificationNote::Code::NONE_ENCRYPTED);
1558 for (auto const& i: cpl->additional_subtitle_languages()) {
1559 verify_language_tag(context, i);
1562 if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1563 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1564 * of the approved ones.
1566 auto all = ContentKind::all();
1567 auto name = cpl->content_kind().name();
1568 transform(name.begin(), name.end(), name.begin(), ::tolower);
1569 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1570 if (iter == all.end()) {
1571 context.error(VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name());
1573 context.ok(VerificationNote::Code::VALID_CONTENT_KIND, cpl->content_kind().name());
1577 if (cpl->release_territory()) {
1578 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") {
1579 auto terr = cpl->release_territory().get();
1581 /* Must be a valid region tag, or "001" */
1583 LanguageTag::RegionSubtag test(terr);
1585 if (terr != "001") {
1586 context.bv21_error(VerificationNote::Code::INVALID_LANGUAGE, terr);
1591 context.ok(VerificationNote::Code::VALID_RELEASE_TERRITORY, terr);
1596 for (auto version: cpl->content_versions()) {
1597 if (version.label_text.empty()) {
1598 context.warning(VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT, cpl->file().get());
1601 context.ok(VerificationNote::Code::VALID_CONTENT_VERSION_LABEL_TEXT, version.label_text);
1605 if (context.dcp->standard() == Standard::SMPTE) {
1606 if (!cpl->annotation_text()) {
1607 context.bv21_error(VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->file().get());
1608 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1609 context.warning(VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->file().get());
1611 context.ok(VerificationNote::Code::VALID_CPL_ANNOTATION_TEXT, cpl->annotation_text().get());
1615 for (auto i: context.dcp->pkls()) {
1616 /* Check that the CPL's hash corresponds to the PKL */
1617 optional<string> h = i->hash(cpl->id());
1618 auto calculated_cpl_hash = make_digest(ArrayData(*cpl->file()));
1619 if (h && calculated_cpl_hash != *h) {
1621 dcp::VerificationNote(
1622 VerificationNote::Type::ERROR,
1623 VerificationNote::Code::MISMATCHED_CPL_HASHES,
1625 ).set_calculated_hash(calculated_cpl_hash).set_reference_hash(*h)
1628 context.ok(VerificationNote::Code::MATCHING_CPL_HASHES);
1631 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1632 optional<string> required_annotation_text;
1633 for (auto j: i->assets()) {
1634 /* See if this is a CPL */
1635 for (auto k: context.dcp->cpls()) {
1636 if (j->id() == k->id()) {
1637 if (!required_annotation_text) {
1638 /* First CPL we have found; this is the required AnnotationText unless we find another */
1639 required_annotation_text = cpl->content_title_text();
1641 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1642 required_annotation_text = boost::none;
1648 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1649 context.bv21_error(VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get());
1651 context.ok(VerificationNote::Code::MATCHING_PKL_ANNOTATION_TEXT_WITH_CPL);
1655 /* set to true if any reel has a MainSubtitle */
1656 auto have_main_subtitle = false;
1657 /* set to true if any reel has no MainSubtitle */
1658 auto have_no_main_subtitle = false;
1659 /* fewest number of closed caption assets seen in a reel */
1660 size_t fewest_closed_captions = SIZE_MAX;
1661 /* most number of closed caption assets seen in a reel */
1662 size_t most_closed_captions = 0;
1663 map<Marker, Time> markers_seen;
1665 auto const main_picture_active_area = cpl->main_picture_active_area();
1666 bool active_area_ok = true;
1667 if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1669 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1670 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1673 active_area_ok = false;
1675 if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1677 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1678 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1681 active_area_ok = false;
1684 if (main_picture_active_area && active_area_ok) {
1686 VerificationNote::Code::VALID_MAIN_PICTURE_ACTIVE_AREA, String::compose("%1x%2", main_picture_active_area->width, main_picture_active_area->height),
1692 for (auto reel: cpl->reels()) {
1693 context.stage("Checking reel", optional<boost::filesystem::path>());
1698 main_picture_active_area,
1699 &have_main_subtitle,
1700 &have_no_main_subtitle,
1701 &most_closed_captions,
1702 &fewest_closed_captions,
1705 frame += reel->duration();
1708 verify_text_details(context, cpl->reels());
1710 if (context.dcp->standard() == Standard::SMPTE) {
1711 if (auto msc = cpl->main_sound_configuration()) {
1712 if (context.audio_channels && msc->channels() != *context.audio_channels) {
1714 VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION,
1715 String::compose("MainSoundConfiguration has %1 channels but sound assets have %2", msc->channels(), *context.audio_channels),
1721 if (have_main_subtitle && have_no_main_subtitle) {
1722 context.bv21_error(VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS);
1725 if (fewest_closed_captions != most_closed_captions) {
1726 context.bv21_error(VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS);
1729 if (cpl->content_kind() == ContentKind::FEATURE) {
1730 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1731 context.bv21_error(VerificationNote::Code::MISSING_FFEC_IN_FEATURE);
1733 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1734 context.bv21_error(VerificationNote::Code::MISSING_FFMC_IN_FEATURE);
1738 auto ffoc = markers_seen.find(Marker::FFOC);
1739 if (ffoc == markers_seen.end()) {
1740 context.warning(VerificationNote::Code::MISSING_FFOC);
1741 } else if (ffoc->second.e != 1) {
1742 context.warning(VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e));
1745 auto lfoc = markers_seen.find(Marker::LFOC);
1746 if (lfoc == markers_seen.end()) {
1747 context.warning(VerificationNote::Code::MISSING_LFOC);
1749 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1750 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1751 context.warning(VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time));
1755 LinesCharactersResult result;
1756 for (auto reel: cpl->reels()) {
1757 if (reel->main_subtitle() && reel->main_subtitle()->asset_ref().resolved()) {
1758 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1762 if (result.line_count_exceeded) {
1763 context.warning(VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT);
1765 if (result.error_length_exceeded) {
1766 context.warning(VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH);
1767 } else if (result.warning_length_exceeded) {
1768 context.warning(VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH);
1771 result = LinesCharactersResult();
1772 for (auto reel: cpl->reels()) {
1773 for (auto i: reel->closed_captions()) {
1775 verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1780 if (result.line_count_exceeded) {
1781 context.bv21_error(VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT);
1783 if (result.error_length_exceeded) {
1784 context.bv21_error(VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH);
1787 if (!cpl->read_composition_metadata()) {
1788 context.bv21_error(VerificationNote::Code::MISSING_CPL_METADATA, cpl->file().get());
1789 } else if (!cpl->version_number()) {
1790 context.bv21_error(VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->file().get());
1793 verify_extension_metadata(context, cpl);
1795 if (cpl->any_encrypted()) {
1796 cxml::Document doc("CompositionPlaylist");
1797 DCP_ASSERT(cpl->file());
1798 doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1799 if (!doc.optional_node_child("Signature")) {
1800 context.bv21_error(VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->file().get());
1809 verify_pkl(Context& context, shared_ptr<const PKL> pkl)
1811 validate_xml(context, pkl->file().get());
1813 if (pkl_has_encrypted_assets(context.dcp, pkl)) {
1814 cxml::Document doc("PackingList");
1815 doc.read_file(dcp::filesystem::fix_long_path(pkl->file().get()));
1816 if (!doc.optional_node_child("Signature")) {
1817 context.bv21_error(VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get());
1821 set<string> uuid_set;
1822 for (auto asset: pkl->assets()) {
1823 if (!uuid_set.insert(asset->id()).second) {
1824 context.error(VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get());
1834 verify_assetmap(Context& context, shared_ptr<const DCP> dcp)
1836 auto asset_map = dcp->asset_map();
1837 DCP_ASSERT(asset_map);
1839 validate_xml(context, asset_map->file().get());
1841 set<string> uuid_set;
1842 for (auto const& asset: asset_map->assets()) {
1843 if (!uuid_set.insert(asset.id()).second) {
1844 context.error(VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get());
1853 vector<boost::filesystem::path> directories,
1854 vector<dcp::DecryptedKDM> kdms,
1855 function<void (string, optional<boost::filesystem::path>)> stage,
1856 function<void (float)> progress,
1857 VerificationOptions options,
1858 optional<boost::filesystem::path> xsd_dtd_directory
1861 if (!xsd_dtd_directory) {
1862 xsd_dtd_directory = resources_directory() / "xsd";
1864 *xsd_dtd_directory = filesystem::canonical(*xsd_dtd_directory);
1866 vector<VerificationNote> notes;
1867 Context context(notes, *xsd_dtd_directory, stage, progress, options);
1869 vector<shared_ptr<DCP>> dcps;
1870 for (auto i: directories) {
1871 dcps.push_back (make_shared<DCP>(i));
1874 for (auto dcp: dcps) {
1875 stage ("Checking DCP", dcp->directory());
1879 bool carry_on = true;
1881 dcp->read (¬es, true);
1882 } catch (MissingAssetmapError& e) {
1883 context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1885 } catch (ReadError& e) {
1886 context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1887 } catch (XMLError& e) {
1888 context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1889 } catch (MXFFileError& e) {
1890 context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1891 } catch (BadURNUUIDError& e) {
1892 context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1893 } catch (cxml::Error& e) {
1894 context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1901 if (dcp->standard() != Standard::SMPTE) {
1902 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1905 for (auto kdm: kdms) {
1909 for (auto cpl: dcp->cpls()) {
1912 verify_cpl(context, cpl);
1913 context.cpl.reset();
1914 } catch (ReadError& e) {
1915 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1919 for (auto pkl: dcp->pkls()) {
1920 stage("Checking PKL", pkl->file());
1921 verify_pkl(context, pkl);
1924 if (dcp->asset_map_file()) {
1925 stage("Checking ASSETMAP", dcp->asset_map_file().get());
1926 verify_assetmap(context, dcp);
1928 context.error(VerificationNote::Code::MISSING_ASSETMAP);
1932 return { notes, dcps };
1937 dcp::note_to_string (VerificationNote note)
1939 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1941 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1942 * not "ClosedCaption assets must have an <EntryPoint> tag."
1944 * It's OK to use XML tag names where they are clear.
1945 * If both ID and filename are available, use only the ID.
1946 * End messages with a full stop.
1947 * Messages should not mention whether or not their errors are a part of Bv2.1.
1949 switch (note.code()) {
1950 case VerificationNote::Code::FAILED_READ:
1951 return *note.note();
1952 case VerificationNote::Code::MATCHING_CPL_HASHES:
1953 return "The hash of the CPL in the PKL matches the CPL file.";
1954 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1955 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());
1956 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1957 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1958 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1959 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());
1960 case VerificationNote::Code::CORRECT_PICTURE_HASH:
1961 return String::compose("The picture asset %1 has the expected hashes in the CPL and PKL.", note.file()->filename());
1962 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1963 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1964 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1965 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());
1966 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1967 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1968 case VerificationNote::Code::EMPTY_ASSET_PATH:
1969 return "The asset map contains an empty asset path.";
1970 case VerificationNote::Code::MISSING_ASSET:
1971 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1972 case VerificationNote::Code::MISMATCHED_STANDARD:
1973 return "The DCP contains both SMPTE and Interop parts.";
1974 case VerificationNote::Code::INVALID_XML:
1975 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1976 case VerificationNote::Code::MISSING_ASSETMAP:
1977 return "No valid ASSETMAP or ASSETMAP.xml was found.";
1978 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1979 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1980 case VerificationNote::Code::INVALID_DURATION:
1981 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1982 case VerificationNote::Code::VALID_PICTURE_FRAME_SIZES_IN_BYTES:
1983 return String::compose("Each frame of the picture asset %1 has a bit rate safely under the limit of 250Mbit/s.", note.file()->filename());
1984 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1985 return String::compose(
1986 "Frame %1 (timecode %2) in asset %3 has an instantaneous bit rate that is larger than the limit of 250Mbit/s.",
1988 dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
1989 note.file()->filename()
1991 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1992 return String::compose(
1993 "Frame %1 (timecode %2) in asset %3 has an instantaneous bit rate that is close to the limit of 250Mbit/s.",
1995 dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
1996 note.file()->filename()
1998 case VerificationNote::Code::EXTERNAL_ASSET:
1999 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());
2000 case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
2001 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
2002 case VerificationNote::Code::INVALID_STANDARD:
2003 return "This DCP does not use the SMPTE standard.";
2004 case VerificationNote::Code::INVALID_LANGUAGE:
2005 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
2006 case VerificationNote::Code::VALID_RELEASE_TERRITORY:
2007 return String::compose("Valid release territory %1.", note.note().get());
2008 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
2009 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
2010 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
2011 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
2012 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
2013 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
2014 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
2015 return "3D 4K DCPs are not allowed.";
2016 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
2017 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
2018 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
2019 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
2020 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
2021 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());
2022 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
2023 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
2024 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
2025 return "Some subtitle assets have different <Language> tags than others";
2026 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
2027 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
2028 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
2029 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
2030 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
2031 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
2032 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
2033 return "At least one subtitle lasts less than 15 frames.";
2034 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
2035 return "At least one pair of subtitles is separated by less than 2 frames.";
2036 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
2037 return "At least one subtitle extends outside of its reel.";
2038 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
2039 return "There are more than 3 subtitle lines in at least one place in the DCP.";
2040 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
2041 return "There are more than 52 characters in at least one subtitle line.";
2042 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
2043 return "There are more than 79 characters in at least one subtitle line.";
2044 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
2045 return "There are more than 3 closed caption lines in at least one place.";
2046 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
2047 return "There are more than 32 characters in at least one closed caption line.";
2048 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
2049 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
2050 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
2051 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.cpl_id().get());
2052 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
2053 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.cpl_id().get());
2054 case VerificationNote::Code::VALID_CPL_ANNOTATION_TEXT:
2055 return String::compose("Valid CPL annotation text %1", note.note().get());
2056 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
2057 return "All assets in a reel do not have the same duration.";
2058 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
2059 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
2060 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
2061 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
2062 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
2063 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
2064 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
2065 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
2066 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
2067 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
2068 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
2069 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
2070 case VerificationNote::Code::MISSING_HASH:
2071 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
2072 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
2073 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
2074 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
2075 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
2076 case VerificationNote::Code::MISSING_FFOC:
2077 return "There should be a FFOC (first frame of content) marker.";
2078 case VerificationNote::Code::MISSING_LFOC:
2079 return "There should be a LFOC (last frame of content) marker.";
2080 case VerificationNote::Code::INCORRECT_FFOC:
2081 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
2082 case VerificationNote::Code::INCORRECT_LFOC:
2083 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
2084 case VerificationNote::Code::MISSING_CPL_METADATA:
2085 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.cpl_id().get());
2086 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
2087 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.cpl_id().get());
2088 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
2089 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.cpl_id().get());
2090 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
2091 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
2092 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
2093 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.cpl_id().get());
2094 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
2095 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
2096 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
2097 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
2098 case VerificationNote::Code::MATCHING_PKL_ANNOTATION_TEXT_WITH_CPL:
2099 return "The PKL and CPL annotation texts match.";
2100 case VerificationNote::Code::ALL_ENCRYPTED:
2101 return "All the assets are encrypted.";
2102 case VerificationNote::Code::NONE_ENCRYPTED:
2103 return "All the assets are unencrypted.";
2104 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
2105 return "Some assets are encrypted but some are not.";
2106 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
2107 return String::compose(
2108 "Frame %1 (timecode %2) has an invalid JPEG2000 codestream (%3).",
2110 dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
2113 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
2114 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
2115 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
2116 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
2117 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
2118 return "The JPEG2000 tile size is not the same as the image size.";
2119 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
2120 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
2121 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
2122 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
2123 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
2124 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
2125 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
2126 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
2127 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
2128 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
2129 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
2130 return "POC marker found outside main header.";
2131 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
2132 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
2133 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
2134 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
2135 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
2136 return "No TLM marker was found in a JPEG2000 codestream.";
2137 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
2138 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
2139 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
2140 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
2141 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
2143 vector<string> parts;
2144 boost::split (parts, note.note().get(), boost::is_any_of(" "));
2145 DCP_ASSERT (parts.size() == 2);
2146 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]);
2148 case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
2149 return "Some aspect of this DCP could not be checked because it is encrypted.";
2150 case VerificationNote::Code::EMPTY_TEXT:
2151 return "There is an empty <Text> node in a subtitle or closed caption.";
2152 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
2153 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
2154 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
2155 return "Some closed captions are not listed in the order of their vertical position.";
2156 case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
2157 return "There is an <EntryPoint> node inside a <MainMarkers>.";
2158 case VerificationNote::Code::UNEXPECTED_DURATION:
2159 return "There is an <Duration> node inside a <MainMarkers>.";
2160 case VerificationNote::Code::INVALID_CONTENT_KIND:
2161 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
2162 case VerificationNote::Code::VALID_CONTENT_KIND:
2163 return String::compose("Valid <ContentKind> %1.", note.note().get());
2164 case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
2165 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
2166 case VerificationNote::Code::VALID_MAIN_PICTURE_ACTIVE_AREA:
2167 return String::compose("<MainPictureActiveaArea> %1 is valid", note.note().get());
2168 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
2169 return String::compose("The PKL %1 has more than one asset with the same ID.", note.note().get());
2170 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
2171 return String::compose("The ASSETMAP %1 has more than one asset with the same ID.", note.note().get());
2172 case VerificationNote::Code::MISSING_SUBTITLE:
2173 return String::compose("The subtitle asset %1 has no subtitles.", note.note().get());
2174 case VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
2175 return String::compose("<IssueDate> has an invalid value: %1", note.note().get());
2176 case VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS:
2177 return String::compose("The sound assets do not all have the same channel count; the first to differ is %1", note.file()->filename());
2178 case VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION:
2179 return String::compose("<MainSoundConfiguration> has an invalid value: %1", note.note().get());
2180 case VerificationNote::Code::MISSING_FONT:
2181 return String::compose("The font file for font ID \"%1\" was not found, or was not referred to in the ASSETMAP.", note.note().get());
2182 case VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE:
2183 return String::compose(
2184 "Frame %1 has an image component that is too large (component %2 is %3 bytes in size).",
2185 note.frame().get(), note.component().get(), note.size().get()
2187 case VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT:
2188 return String::compose("The XML in the subtitle asset %1 has more than one namespace declaration.", note.note().get());
2189 case VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT:
2190 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());
2191 case VerificationNote::Code::MISSING_LOAD_FONT:
2192 return String::compose("The SMPTE subtitle asset %1 has <Text> nodes but no <LoadFont> node", note.id().get());
2193 case VerificationNote::Code::MISMATCHED_ASSET_MAP_ID:
2194 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());
2195 case VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT:
2196 return String::compose("The <LabelText> in a <ContentVersion> in CPL %1 is empty", note.cpl_id().get());
2197 case VerificationNote::Code::VALID_CONTENT_VERSION_LABEL_TEXT:
2198 return String::compose("CPL has valid <ContentVersion> %1", note.note().get());
2206 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2208 return a.type() == b.type() &&
2209 a.code() == b.code() &&
2210 a.note() == b.note() &&
2211 a.file() == b.file() &&
2212 a.line() == b.line() &&
2213 a.frame() == b.frame() &&
2214 a.component() == b.component() &&
2215 a.size() == b.size() &&
2217 a.other_id() == b.other_id() &&
2218 a.frame_rate() == b.frame_rate() &&
2219 a.cpl_id() == b.cpl_id() &&
2220 a.reference_hash() == b.reference_hash() &&
2221 a.calculated_hash() == b.calculated_hash();
2226 dcp::operator!=(dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2233 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2235 if (a.type() != b.type()) {
2236 return a.type() < b.type();
2239 if (a.code() != b.code()) {
2240 return a.code() < b.code();
2243 if (a.note() != b.note()) {
2244 return a.note().get_value_or("") < b.note().get_value_or("");
2247 if (a.file() != b.file()) {
2248 return a.file().get_value_or("") < b.file().get_value_or("");
2251 if (a.line() != b.line()) {
2252 return a.line().get_value_or(0) < b.line().get_value_or(0);
2255 if (a.frame() != b.frame()) {
2256 return a.frame().get_value_or(0) < b.frame().get_value_or(0);
2259 if (a.component() != b.component()) {
2260 return a.component().get_value_or(0) < b.component().get_value_or(0);
2263 if (a.size() != b.size()) {
2264 return a.size().get_value_or(0) < b.size().get_value_or(0);
2267 if (a.id() != b.id()) {
2268 return a.id().get_value_or("") < b.id().get_value_or("");
2271 if (a.other_id() != b.other_id()) {
2272 return a.other_id().get_value_or("") < b.other_id().get_value_or("");
2275 return a.frame_rate().get_value_or(0) != b.frame_rate().get_value_or(0);
2280 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2282 s << note_to_string (note);
2284 s << " [" << note.note().get() << "]";
2287 s << " [" << note.file().get() << "]";
2290 s << " [" << note.line().get() << "]";