2 Copyright (C) 2018-2021 Carl Hetherington <cth@carlh.net>
4 This file is part of libdcp.
6 libdcp is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
11 libdcp is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with libdcp. If not, see <http://www.gnu.org/licenses/>.
19 In addition, as a special exception, the copyright holders give
20 permission to link the code of portions of this program with the
21 OpenSSL library under certain conditions as described in each
22 individual source file, and distribute linked combinations
25 You must obey the GNU General Public License in all respects
26 for all of the code used other than OpenSSL. If you modify
27 file(s) with this exception, you may extend this exception to your
28 version of the file(s), but you are not obligated to do so. If you
29 do not wish to do so, delete this exception statement from your
30 version. If you delete this exception statement from all source
31 files in the program, then also delete it here.
35 /** @file src/verify.cc
36 * @brief dcp::verify() method and associated code
40 #include "compose.hpp"
43 #include "exceptions.h"
44 #include "filesystem.h"
45 #include "interop_subtitle_asset.h"
46 #include "mono_picture_asset.h"
47 #include "mono_picture_frame.h"
48 #include "raw_convert.h"
50 #include "reel_closed_caption_asset.h"
51 #include "reel_interop_subtitle_asset.h"
52 #include "reel_markers_asset.h"
53 #include "reel_picture_asset.h"
54 #include "reel_sound_asset.h"
55 #include "reel_smpte_subtitle_asset.h"
56 #include "reel_subtitle_asset.h"
57 #include "smpte_subtitle_asset.h"
58 #include "stereo_picture_asset.h"
59 #include "stereo_picture_frame.h"
61 #include "verify_j2k.h"
62 #include <libxml/parserInternals.h>
63 #include <xercesc/dom/DOMAttr.hpp>
64 #include <xercesc/dom/DOMDocument.hpp>
65 #include <xercesc/dom/DOMError.hpp>
66 #include <xercesc/dom/DOMErrorHandler.hpp>
67 #include <xercesc/dom/DOMException.hpp>
68 #include <xercesc/dom/DOMImplementation.hpp>
69 #include <xercesc/dom/DOMImplementationLS.hpp>
70 #include <xercesc/dom/DOMImplementationRegistry.hpp>
71 #include <xercesc/dom/DOMLSParser.hpp>
72 #include <xercesc/dom/DOMLocator.hpp>
73 #include <xercesc/dom/DOMNamedNodeMap.hpp>
74 #include <xercesc/dom/DOMNodeList.hpp>
75 #include <xercesc/framework/LocalFileInputSource.hpp>
76 #include <xercesc/framework/MemBufInputSource.hpp>
77 #include <xercesc/parsers/AbstractDOMParser.hpp>
78 #include <xercesc/parsers/XercesDOMParser.hpp>
79 #include <xercesc/sax/HandlerBase.hpp>
80 #include <xercesc/util/PlatformUtils.hpp>
81 #include <boost/algorithm/string.hpp>
90 using std::dynamic_pointer_cast;
92 using std::make_shared;
96 using std::shared_ptr;
99 using boost::optional;
100 using boost::function;
104 using namespace xercesc;
109 xml_ch_to_string (XMLCh const * a)
111 char* x = XMLString::transcode(a);
113 XMLString::release(&x);
118 class XMLValidationError
121 XMLValidationError (SAXParseException const & e)
122 : _message (xml_ch_to_string(e.getMessage()))
123 , _line (e.getLineNumber())
124 , _column (e.getColumnNumber())
125 , _public_id (e.getPublicId() ? xml_ch_to_string(e.getPublicId()) : "")
126 , _system_id (e.getSystemId() ? xml_ch_to_string(e.getSystemId()) : "")
131 string message () const {
135 uint64_t line () const {
139 uint64_t column () const {
143 string public_id () const {
147 string system_id () const {
160 class DCPErrorHandler : public ErrorHandler
163 void warning(const SAXParseException& e) override
165 maybe_add (XMLValidationError(e));
168 void error(const SAXParseException& e) override
170 maybe_add (XMLValidationError(e));
173 void fatalError(const SAXParseException& e) override
175 maybe_add (XMLValidationError(e));
178 void resetErrors() override {
182 list<XMLValidationError> errors () const {
187 void maybe_add (XMLValidationError e)
189 /* XXX: nasty hack */
191 e.message().find("schema document") != string::npos &&
192 e.message().find("has different target namespace from the one specified in instance document") != string::npos
197 _errors.push_back (e);
200 list<XMLValidationError> _errors;
207 StringToXMLCh (string a)
209 _buffer = XMLString::transcode(a.c_str());
212 StringToXMLCh (StringToXMLCh const&) = delete;
213 StringToXMLCh& operator= (StringToXMLCh const&) = delete;
217 XMLString::release (&_buffer);
220 XMLCh const * get () const {
229 class LocalFileResolver : public EntityResolver
232 LocalFileResolver (boost::filesystem::path xsd_dtd_directory)
233 : _xsd_dtd_directory (xsd_dtd_directory)
235 /* XXX: I'm not clear on what things need to be in this list; some XSDs are apparently, magically
236 * found without being here.
238 add("http://www.w3.org/2001/XMLSchema.dtd", "XMLSchema.dtd");
239 add("http://www.w3.org/2001/03/xml.xsd", "xml.xsd");
240 add("http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd", "xmldsig-core-schema.xsd");
241 add("http://www.digicine.com/schemas/437-Y/2007/Main-Stereo-Picture-CPL.xsd", "Main-Stereo-Picture-CPL.xsd");
242 add("http://www.digicine.com/PROTO-ASDCP-CPL-20040511.xsd", "PROTO-ASDCP-CPL-20040511.xsd");
243 add("http://www.digicine.com/PROTO-ASDCP-PKL-20040311.xsd", "PROTO-ASDCP-PKL-20040311.xsd");
244 add("http://www.digicine.com/PROTO-ASDCP-AM-20040311.xsd", "PROTO-ASDCP-AM-20040311.xsd");
245 add("http://www.digicine.com/PROTO-ASDCP-CC-CPL-20070926#", "PROTO-ASDCP-CC-CPL-20070926.xsd");
246 add("interop-subs", "DCSubtitle.v1.mattsson.xsd");
247 add("http://www.smpte-ra.org/schemas/428-7/2010/DCST.xsd", "DCDMSubtitle-2010.xsd");
248 add("http://www.smpte-ra.org/schemas/428-7/2014/DCST.xsd", "DCDMSubtitle-2014.xsd");
249 add("http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata", "SMPTE-429-16.xsd");
250 add("http://www.dolby.com/schemas/2012/AD", "Dolby-2012-AD.xsd");
251 add("http://www.smpte-ra.org/schemas/429-10/2008/Main-Stereo-Picture-CPL", "SMPTE-429-10-2008.xsd");
254 InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id) override
259 auto system_id_str = xml_ch_to_string (system_id);
260 auto p = _xsd_dtd_directory;
261 if (_files.find(system_id_str) == _files.end()) {
264 p /= _files[system_id_str];
266 StringToXMLCh ch (p.string());
267 return new LocalFileInputSource(ch.get());
271 void add (string uri, string file)
276 std::map<string, string> _files;
277 boost::filesystem::path _xsd_dtd_directory;
282 parse (XercesDOMParser& parser, boost::filesystem::path xml)
284 parser.parse(xml.c_str());
289 parse (XercesDOMParser& parser, string xml)
291 xercesc::MemBufInputSource buf(reinterpret_cast<unsigned char const*>(xml.c_str()), xml.size(), "");
298 validate_xml (T xml, boost::filesystem::path xsd_dtd_directory, vector<VerificationNote>& notes)
301 XMLPlatformUtils::Initialize ();
302 } catch (XMLException& e) {
303 throw MiscError ("Failed to initialise xerces library");
306 DCPErrorHandler error_handler;
308 /* All the xerces objects in this scope must be destroyed before XMLPlatformUtils::Terminate() is called */
310 XercesDOMParser parser;
311 parser.setValidationScheme(XercesDOMParser::Val_Always);
312 parser.setDoNamespaces(true);
313 parser.setDoSchema(true);
315 vector<string> schema;
316 schema.push_back("xml.xsd");
317 schema.push_back("xmldsig-core-schema.xsd");
318 schema.push_back("SMPTE-429-7-2006-CPL.xsd");
319 schema.push_back("SMPTE-429-8-2006-PKL.xsd");
320 schema.push_back("SMPTE-429-9-2007-AM.xsd");
321 schema.push_back("Main-Stereo-Picture-CPL.xsd");
322 schema.push_back("PROTO-ASDCP-CPL-20040511.xsd");
323 schema.push_back("PROTO-ASDCP-PKL-20040311.xsd");
324 schema.push_back("PROTO-ASDCP-AM-20040311.xsd");
325 schema.push_back("DCSubtitle.v1.mattsson.xsd");
326 schema.push_back("DCDMSubtitle-2010.xsd");
327 schema.push_back("DCDMSubtitle-2014.xsd");
328 schema.push_back("PROTO-ASDCP-CC-CPL-20070926.xsd");
329 schema.push_back("SMPTE-429-16.xsd");
330 schema.push_back("Dolby-2012-AD.xsd");
331 schema.push_back("SMPTE-429-10-2008.xsd");
332 schema.push_back("xlink.xsd");
333 schema.push_back("SMPTE-335-2012.xsd");
334 schema.push_back("SMPTE-395-2014-13-1-aaf.xsd");
335 schema.push_back("isdcf-mca.xsd");
336 schema.push_back("SMPTE-429-12-2008.xsd");
338 /* XXX: I'm not especially clear what this is for, but it seems to be necessary.
339 * Schemas that are not mentioned in this list are not read, and the things
340 * they describe are not checked.
343 for (auto i: schema) {
344 locations += String::compose("%1 %1 ", i, i);
347 parser.setExternalSchemaLocation(locations.c_str());
348 parser.setValidationSchemaFullChecking(true);
349 parser.setErrorHandler(&error_handler);
351 LocalFileResolver resolver (xsd_dtd_directory);
352 parser.setEntityResolver(&resolver);
355 parser.resetDocumentPool();
357 } catch (XMLException& e) {
358 throw MiscError(xml_ch_to_string(e.getMessage()));
359 } catch (DOMException& e) {
360 throw MiscError(xml_ch_to_string(e.getMessage()));
362 throw MiscError("Unknown exception from xerces");
366 XMLPlatformUtils::Terminate ();
368 for (auto i: error_handler.errors()) {
370 VerificationNote::Type::ERROR,
371 VerificationNote::Code::INVALID_XML,
373 boost::trim_copy(i.public_id() + " " + i.system_id()),
380 enum class VerifyAssetResult {
387 static VerifyAssetResult
389 shared_ptr<const DCP> dcp,
390 shared_ptr<const ReelFileAsset> reel_file_asset,
391 function<void (float)> progress,
392 string* reference_hash,
393 string* calculated_hash
396 DCP_ASSERT(reference_hash);
397 DCP_ASSERT(calculated_hash);
399 /* When reading the DCP the hash will have been set to the one from the PKL/CPL.
400 * We want to calculate the hash of the actual file contents here, so that we
401 * can check it. unset_hash() means that this calculation will happen on the
404 reel_file_asset->asset_ref()->unset_hash();
405 *calculated_hash = reel_file_asset->asset_ref()->hash([progress](int64_t done, int64_t total) {
406 progress(float(done) / total);
409 auto pkls = dcp->pkls();
410 /* We've read this DCP in so it must have at least one PKL */
411 DCP_ASSERT (!pkls.empty());
413 auto asset = reel_file_asset->asset_ref().asset();
415 optional<string> maybe_pkl_hash;
417 maybe_pkl_hash = i->hash (reel_file_asset->asset_ref()->id());
418 if (maybe_pkl_hash) {
423 DCP_ASSERT(maybe_pkl_hash);
424 *reference_hash = *maybe_pkl_hash;
426 auto cpl_hash = reel_file_asset->hash();
427 if (cpl_hash && *cpl_hash != *reference_hash) {
428 return VerifyAssetResult::CPL_PKL_DIFFER;
431 if (*calculated_hash != *reference_hash) {
432 return VerifyAssetResult::BAD;
435 return VerifyAssetResult::GOOD;
440 verify_language_tag (string tag, vector<VerificationNote>& notes)
443 LanguageTag test (tag);
444 } catch (LanguageTagError &) {
445 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, tag});
451 verify_picture_asset(shared_ptr<const ReelFileAsset> reel_file_asset, boost::filesystem::path file, int64_t start_frame, vector<VerificationNote>& notes, function<void (float)> progress)
453 auto asset = dynamic_pointer_cast<PictureAsset>(reel_file_asset->asset_ref().asset());
454 auto const duration = asset->intrinsic_duration ();
456 auto check_and_add = [¬es](vector<VerificationNote> const& j2k_notes) {
457 for (auto i: j2k_notes) {
458 if (find(notes.begin(), notes.end(), i) == notes.end()) {
464 int const max_frame = rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
465 int const risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
467 auto check_frame_size = [max_frame, risky_frame, file, start_frame](int index, int size, int frame_rate, vector<VerificationNote>& notes) {
468 if (size > max_frame) {
471 VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
472 ).set_frame(start_frame + index).set_frame_rate(frame_rate)
474 } else if (size > risky_frame) {
477 VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
478 ).set_frame(start_frame + index).set_frame_rate(frame_rate)
483 if (auto mono_asset = dynamic_pointer_cast<MonoPictureAsset>(reel_file_asset->asset_ref().asset())) {
484 auto reader = mono_asset->start_read ();
485 for (int64_t i = 0; i < duration; ++i) {
486 auto frame = reader->get_frame (i);
487 check_frame_size(i, frame->size(), mono_asset->frame_rate().numerator, notes);
488 if (!mono_asset->encrypted() || mono_asset->key()) {
489 vector<VerificationNote> j2k_notes;
490 verify_j2k(frame, start_frame, i, mono_asset->frame_rate().numerator, j2k_notes);
491 check_and_add (j2k_notes);
493 progress (float(i) / duration);
495 } else if (auto stereo_asset = dynamic_pointer_cast<StereoPictureAsset>(asset)) {
496 auto reader = stereo_asset->start_read ();
497 for (int64_t i = 0; i < duration; ++i) {
498 auto frame = reader->get_frame (i);
499 check_frame_size(i, frame->left()->size(), stereo_asset->frame_rate().numerator, notes);
500 check_frame_size(i, frame->right()->size(), stereo_asset->frame_rate().numerator, notes);
501 if (!stereo_asset->encrypted() || stereo_asset->key()) {
502 vector<VerificationNote> j2k_notes;
503 verify_j2k(frame->left(), start_frame, i, stereo_asset->frame_rate().numerator, j2k_notes);
504 verify_j2k(frame->right(), start_frame, i, stereo_asset->frame_rate().numerator, j2k_notes);
505 check_and_add (j2k_notes);
507 progress (float(i) / duration);
515 verify_main_picture_asset (
516 shared_ptr<const DCP> dcp,
517 shared_ptr<const ReelPictureAsset> reel_asset,
519 function<void (string, optional<boost::filesystem::path>)> stage,
520 function<void (float)> progress,
521 VerificationOptions options,
522 vector<VerificationNote>& notes
525 auto asset = reel_asset->asset();
526 auto const file = *asset->file();
528 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
529 stage ("Checking picture asset hash", file);
530 string reference_hash;
531 string calculated_hash;
532 auto const r = verify_asset(dcp, reel_asset, progress, &reference_hash, &calculated_hash);
534 case VerifyAssetResult::BAD:
536 dcp::VerificationNote(
537 VerificationNote::Type::ERROR,
538 VerificationNote::Code::INCORRECT_PICTURE_HASH,
540 ).set_reference_hash(reference_hash).set_calculated_hash(calculated_hash)
543 case VerifyAssetResult::CPL_PKL_DIFFER:
545 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file
553 stage ("Checking picture frame sizes", asset->file());
554 verify_picture_asset(reel_asset, file, start_frame, notes, progress);
556 /* Only flat/scope allowed by Bv2.1 */
558 asset->size() != Size(2048, 858) &&
559 asset->size() != Size(1998, 1080) &&
560 asset->size() != Size(4096, 1716) &&
561 asset->size() != Size(3996, 2160)) {
563 VerificationNote::Type::BV21_ERROR,
564 VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS,
565 String::compose("%1x%2", asset->size().width, asset->size().height),
570 /* Only 24, 25, 48fps allowed for 2K */
572 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
573 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
576 VerificationNote::Type::BV21_ERROR,
577 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
578 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
583 if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
584 /* Only 24fps allowed for 4K */
585 if (asset->edit_rate() != Fraction(24, 1)) {
587 VerificationNote::Type::BV21_ERROR,
588 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
589 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
594 /* Only 2D allowed for 4K */
595 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
597 VerificationNote::Type::BV21_ERROR,
598 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
599 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
611 boost::optional<string> subtitle_language;
612 boost::optional<int> audio_channels;
617 verify_main_sound_asset (
618 shared_ptr<const DCP> dcp,
619 shared_ptr<const ReelSoundAsset> reel_asset,
620 function<void (string, optional<boost::filesystem::path>)> stage,
621 function<void (float)> progress,
622 VerificationOptions options,
623 vector<VerificationNote>& notes,
627 auto asset = reel_asset->asset();
628 auto const file = *asset->file();
630 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
631 stage("Checking sound asset hash", file);
632 string reference_hash;
633 string calculated_hash;
634 auto const r = verify_asset(dcp, reel_asset, progress, &reference_hash, &calculated_hash);
636 case VerifyAssetResult::BAD:
638 dcp::VerificationNote(
639 VerificationNote::Type::ERROR,
640 VerificationNote::Code::INCORRECT_SOUND_HASH,
642 ).set_reference_hash(reference_hash).set_calculated_hash(calculated_hash)
645 case VerifyAssetResult::CPL_PKL_DIFFER:
646 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, file});
653 if (!state.audio_channels) {
654 state.audio_channels = asset->channels();
655 } else if (*state.audio_channels != asset->channels()) {
656 notes.push_back({ VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS, file });
659 stage ("Checking sound asset metadata", file);
661 if (auto lang = asset->language()) {
662 verify_language_tag (*lang, notes);
664 if (asset->sampling_rate() != 48000) {
665 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), file});
671 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
673 /* XXX: is Language compulsory? */
674 if (reel_asset->language()) {
675 verify_language_tag (*reel_asset->language(), notes);
678 if (!reel_asset->entry_point()) {
679 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id() });
680 } else if (reel_asset->entry_point().get()) {
681 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
687 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
689 /* XXX: is Language compulsory? */
690 if (reel_asset->language()) {
691 verify_language_tag (*reel_asset->language(), notes);
694 if (!reel_asset->entry_point()) {
695 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
696 } else if (reel_asset->entry_point().get()) {
697 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
702 /** Verify stuff that is common to both subtitles and closed captions */
704 verify_smpte_timed_text_asset (
705 shared_ptr<const SMPTESubtitleAsset> asset,
706 optional<int64_t> reel_asset_duration,
707 vector<VerificationNote>& notes
710 if (asset->language()) {
711 verify_language_tag (*asset->language(), notes);
713 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
716 auto const size = filesystem::file_size(asset->file().get());
717 if (size > 115 * 1024 * 1024) {
719 { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file() }
723 /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
724 * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
726 auto fonts = asset->font_data ();
728 for (auto i: fonts) {
729 total_size += i.second.size();
731 if (total_size > 10 * 1024 * 1024) {
732 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
735 if (!asset->start_time()) {
736 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get() });
737 } else if (asset->start_time() != Time()) {
738 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() });
741 if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
744 VerificationNote::Type::BV21_ERROR,
745 VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
746 String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
753 /** Verify Interop subtitle / CCAP stuff */
755 verify_interop_text_asset(shared_ptr<const InteropSubtitleAsset> asset, vector<VerificationNote>& notes)
757 if (asset->subtitles().empty()) {
758 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_SUBTITLE, asset->id(), asset->file().get() });
760 auto const unresolved = asset->unresolved_fonts();
761 if (!unresolved.empty()) {
762 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_FONT, unresolved.front() });
767 /** Verify SMPTE subtitle-only stuff */
769 verify_smpte_subtitle_asset (
770 shared_ptr<const SMPTESubtitleAsset> asset,
771 vector<VerificationNote>& notes,
775 if (asset->language()) {
776 if (!state.subtitle_language) {
777 state.subtitle_language = *asset->language();
778 } else if (state.subtitle_language != *asset->language()) {
779 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
783 DCP_ASSERT (asset->resource_id());
784 auto xml_id = asset->xml_id();
786 if (asset->resource_id().get() != xml_id) {
787 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
790 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
791 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
794 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
797 if (asset->raw_xml()) {
798 /* Deluxe require this in their QC even if it seems never to be mentioned in any standard */
799 cxml::Document doc("SubtitleReel");
800 doc.read_string(*asset->raw_xml());
801 auto issue_date = doc.string_child("IssueDate");
802 std::regex reg("^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d$");
803 if (!std::regex_match(issue_date, reg)) {
804 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, issue_date});
810 /** Verify all subtitle stuff */
812 verify_subtitle_asset (
813 shared_ptr<const SubtitleAsset> asset,
814 optional<int64_t> reel_asset_duration,
815 function<void (string, optional<boost::filesystem::path>)> stage,
816 boost::filesystem::path xsd_dtd_directory,
817 vector<VerificationNote>& notes,
821 stage ("Checking subtitle XML", asset->file());
822 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
823 * gets passed through libdcp which may clean up and therefore hide errors.
825 if (asset->raw_xml()) {
826 validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
828 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
831 auto namespace_count = [](shared_ptr<const SubtitleAsset> asset, string root_node) {
832 cxml::Document doc(root_node);
833 doc.read_string(asset->raw_xml().get());
834 auto root = dynamic_cast<xmlpp::Element*>(doc.node())->cobj();
836 for (auto ns = root->nsDef; ns != nullptr; ns = ns->next) {
842 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
844 verify_interop_text_asset(interop, notes);
845 if (namespace_count(asset, "DCSubtitle") > 1) {
846 notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id() });
850 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
852 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
853 verify_smpte_subtitle_asset (smpte, notes, state);
854 /* This asset may be encrypted and in that case we'll have no raw_xml() */
855 if (asset->raw_xml() && namespace_count(asset, "SubtitleReel") > 1) {
856 notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id()});
862 /** Verify all closed caption stuff */
864 verify_closed_caption_asset (
865 shared_ptr<const SubtitleAsset> asset,
866 optional<int64_t> reel_asset_duration,
867 function<void (string, optional<boost::filesystem::path>)> stage,
868 boost::filesystem::path xsd_dtd_directory,
869 vector<VerificationNote>& notes
872 stage ("Checking closed caption XML", asset->file());
873 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
874 * gets passed through libdcp which may clean up and therefore hide errors.
876 auto raw_xml = asset->raw_xml();
878 validate_xml (*raw_xml, xsd_dtd_directory, notes);
879 if (raw_xml->size() > 256 * 1024) {
880 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
883 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
886 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
888 verify_interop_text_asset(interop, notes);
891 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
893 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
898 /** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes etc. */
901 verify_text_details (
902 dcp::Standard standard,
903 vector<shared_ptr<Reel>> reels,
905 vector<VerificationNote>& notes,
906 std::function<bool (shared_ptr<Reel>)> check,
907 std::function<optional<string> (shared_ptr<Reel>)> xml,
908 std::function<int64_t (shared_ptr<Reel>)> duration,
909 std::function<std::string (shared_ptr<Reel>)> id
912 /* end of last subtitle (in editable units) */
913 optional<int64_t> last_out;
914 auto too_short = false;
915 auto too_close = false;
916 auto too_early = false;
917 auto reel_overlap = false;
918 auto empty_text = false;
919 /* current reel start time (in editable units) */
920 int64_t reel_offset = 0;
921 optional<string> missing_load_font_id;
923 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool, bool&, vector<string>&)> parse;
925 parse = [&parse, &last_out, &too_short, &too_close, &too_early, &empty_text, &reel_offset, &missing_load_font_id](
926 cxml::ConstNodePtr node,
928 optional<Time> start_time,
932 vector<string>& font_ids
934 if (node->name() == "Subtitle") {
935 Time in (node->string_attribute("TimeIn"), tcr);
939 Time out (node->string_attribute("TimeOut"), tcr);
943 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
946 auto length = out - in;
947 if (length.as_editable_units_ceil(er) < 15) {
951 /* XXX: this feels dubious - is it really what Bv2.1 means? */
952 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
953 if (distance >= 0 && distance < 2) {
957 last_out = reel_offset + out.as_editable_units_floor(er);
958 } else if (node->name() == "Text") {
959 std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
960 if (!node->content().empty()) {
963 for (auto i: node->node_children()) {
964 if (node_has_content(i)) {
970 if (!node_has_content(node)) {
974 } else if (node->name() == "LoadFont") {
975 if (auto const id = node->optional_string_attribute("Id")) {
976 font_ids.push_back(*id);
977 } else if (auto const id = node->optional_string_attribute("ID")) {
978 font_ids.push_back(*id);
980 } else if (node->name() == "Font") {
981 if (auto const font_id = node->optional_string_attribute("Id")) {
982 if (std::find_if(font_ids.begin(), font_ids.end(), [font_id](string const& id) { return id == font_id; }) == font_ids.end()) {
983 missing_load_font_id = font_id;
987 for (auto i: node->node_children()) {
988 parse(i, tcr, start_time, er, first_reel, has_text, font_ids);
992 for (auto i = 0U; i < reels.size(); ++i) {
993 if (!check(reels[i])) {
997 auto reel_xml = xml(reels[i]);
999 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
1003 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1004 * read in by libdcp's parser.
1007 shared_ptr<cxml::Document> doc;
1009 optional<Time> start_time;
1011 case dcp::Standard::INTEROP:
1012 doc = make_shared<cxml::Document>("DCSubtitle");
1013 doc->read_string (*reel_xml);
1015 case dcp::Standard::SMPTE:
1016 doc = make_shared<cxml::Document>("SubtitleReel");
1017 doc->read_string (*reel_xml);
1018 tcr = doc->number_child<int>("TimeCodeRate");
1019 if (auto start_time_string = doc->optional_string_child("StartTime")) {
1020 start_time = Time(*start_time_string, tcr);
1024 bool has_text = false;
1025 vector<string> font_ids;
1026 parse(doc, tcr, start_time, edit_rate, i == 0, has_text, font_ids);
1027 auto end = reel_offset + duration(reels[i]);
1028 if (last_out && *last_out > end) {
1029 reel_overlap = true;
1033 if (standard == dcp::Standard::SMPTE && has_text && font_ids.empty()) {
1034 notes.push_back(dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT).set_id(id(reels[i])));
1038 if (last_out && *last_out > reel_offset) {
1039 reel_overlap = true;
1044 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
1050 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
1056 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
1062 VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
1068 VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_TEXT
1072 if (missing_load_font_id) {
1073 notes.push_back(dcp::VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT).set_id(*missing_load_font_id));
1080 verify_closed_caption_details (
1081 vector<shared_ptr<Reel>> reels,
1082 vector<VerificationNote>& notes
1085 std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
1086 find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
1087 for (auto i: node->node_children()) {
1088 if (i->name() == "Text") {
1089 text_or_image.push_back (i);
1091 find_text_or_image (i, text_or_image);
1096 auto mismatched_valign = false;
1097 auto incorrect_order = false;
1099 std::function<void (cxml::ConstNodePtr)> parse;
1100 parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
1101 if (node->name() == "Subtitle") {
1102 vector<cxml::ConstNodePtr> text_or_image;
1103 find_text_or_image (node, text_or_image);
1104 optional<string> last_valign;
1105 optional<float> last_vpos;
1106 for (auto i: text_or_image) {
1107 auto valign = i->optional_string_attribute("VAlign");
1109 valign = i->optional_string_attribute("Valign").get_value_or("center");
1111 auto vpos = i->optional_number_attribute<float>("VPosition");
1113 vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
1117 if (*last_valign != valign) {
1118 mismatched_valign = true;
1121 last_valign = valign;
1123 if (!mismatched_valign) {
1125 if (*last_valign == "top" || *last_valign == "center") {
1126 if (*vpos < *last_vpos) {
1127 incorrect_order = true;
1130 if (*vpos > *last_vpos) {
1131 incorrect_order = true;
1140 for (auto i: node->node_children()) {
1145 for (auto reel: reels) {
1146 for (auto ccap: reel->closed_captions()) {
1147 auto reel_xml = ccap->asset()->raw_xml();
1149 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
1153 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1154 * read in by libdcp's parser.
1157 shared_ptr<cxml::Document> doc;
1159 optional<Time> start_time;
1161 doc = make_shared<cxml::Document>("SubtitleReel");
1162 doc->read_string (*reel_xml);
1164 doc = make_shared<cxml::Document>("DCSubtitle");
1165 doc->read_string (*reel_xml);
1171 if (mismatched_valign) {
1173 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN,
1177 if (incorrect_order) {
1179 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING,
1186 dcp::verify_text_lines_and_characters(
1187 shared_ptr<const SubtitleAsset> asset,
1190 LinesCharactersResult* result
1196 Event (Time time_, float position_, int characters_)
1198 , position (position_)
1199 , characters (characters_)
1202 Event (Time time_, shared_ptr<Event> start_)
1208 int position = 0; ///< vertical position from 0 at top of screen to 100 at bottom
1209 int characters = 0; ///< number of characters in the text of this event
1210 shared_ptr<Event> start;
1213 vector<shared_ptr<Event>> events;
1215 auto position = [](shared_ptr<const SubtitleString> sub) {
1216 switch (sub->v_align()) {
1218 return lrintf(sub->v_position() * 100);
1219 case VAlign::CENTER:
1220 return lrintf((0.5f + sub->v_position()) * 100);
1221 case VAlign::BOTTOM:
1222 return lrintf((1.0f - sub->v_position()) * 100);
1228 /* Make a list of "subtitle starts" and "subtitle ends" events */
1229 for (auto j: asset->subtitles()) {
1230 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1232 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1233 events.push_back(in);
1234 events.push_back(make_shared<Event>(text->out(), in));
1238 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1239 return a->time < b->time;
1242 map<int, int> current;
1243 for (auto i: events) {
1244 if (current.size() > 3) {
1245 result->line_count_exceeded = true;
1247 for (auto j: current) {
1248 if (j.second > warning_length) {
1249 result->warning_length_exceeded = true;
1251 if (j.second > error_length) {
1252 result->error_length_exceeded = true;
1257 /* end of a subtitle */
1258 DCP_ASSERT (current.find(i->start->position) != current.end());
1259 if (current[i->start->position] == i->start->characters) {
1260 current.erase(i->start->position);
1262 current[i->start->position] -= i->start->characters;
1265 /* start of a subtitle */
1266 if (current.find(i->position) == current.end()) {
1267 current[i->position] = i->characters;
1269 current[i->position] += i->characters;
1278 verify_text_details(dcp::Standard standard, vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1280 if (reels.empty()) {
1284 if (reels[0]->main_subtitle() && reels[0]->main_subtitle()->asset_ref().resolved()) {
1285 verify_text_details(standard, reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1286 [](shared_ptr<Reel> reel) {
1287 return static_cast<bool>(reel->main_subtitle());
1289 [](shared_ptr<Reel> reel) {
1290 return reel->main_subtitle()->asset()->raw_xml();
1292 [](shared_ptr<Reel> reel) {
1293 return reel->main_subtitle()->actual_duration();
1295 [](shared_ptr<Reel> reel) {
1296 return reel->main_subtitle()->id();
1301 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1302 verify_text_details(standard, reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1303 [i](shared_ptr<Reel> reel) {
1304 return i < reel->closed_captions().size();
1306 [i](shared_ptr<Reel> reel) {
1307 return reel->closed_captions()[i]->asset()->raw_xml();
1309 [i](shared_ptr<Reel> reel) {
1310 return reel->closed_captions()[i]->actual_duration();
1312 [i](shared_ptr<Reel> reel) {
1313 return reel->closed_captions()[i]->id();
1318 verify_closed_caption_details (reels, notes);
1323 verify_extension_metadata(shared_ptr<const CPL> cpl, vector<VerificationNote>& notes)
1325 DCP_ASSERT (cpl->file());
1326 cxml::Document doc ("CompositionPlaylist");
1327 doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1329 auto missing = false;
1332 if (auto reel_list = doc.node_child("ReelList")) {
1333 auto reels = reel_list->node_children("Reel");
1334 if (!reels.empty()) {
1335 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1336 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1337 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1339 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1340 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1344 if (auto name = extension->optional_node_child("Name")) {
1345 if (name->content() != "Application") {
1346 malformed = "<Name> should be 'Application'";
1349 if (auto property_list = extension->optional_node_child("PropertyList")) {
1350 if (auto property = property_list->optional_node_child("Property")) {
1351 if (auto name = property->optional_node_child("Name")) {
1352 if (name->content() != "DCP Constraints Profile") {
1353 malformed = "<Name> property should be 'DCP Constraints Profile'";
1356 if (auto value = property->optional_node_child("Value")) {
1357 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1358 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1373 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1374 } else if (!malformed.empty()) {
1375 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1381 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1383 vector<string> encrypted;
1384 for (auto i: dcp->cpls()) {
1385 for (auto j: i->reel_file_assets()) {
1386 if (j->asset_ref().resolved()) {
1387 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1388 if (mxf && mxf->encrypted()) {
1389 encrypted.push_back(j->asset_ref().id());
1395 for (auto i: pkl->assets()) {
1396 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1408 shared_ptr<const DCP> dcp,
1409 shared_ptr<const CPL> cpl,
1410 shared_ptr<const Reel> reel,
1411 int64_t start_frame,
1412 optional<dcp::Size> main_picture_active_area,
1413 function<void (string, optional<boost::filesystem::path>)> stage,
1414 boost::filesystem::path xsd_dtd_directory,
1415 function<void (float)> progress,
1416 VerificationOptions options,
1417 vector<VerificationNote>& notes,
1419 bool* have_main_subtitle,
1420 bool* have_no_main_subtitle,
1421 size_t* most_closed_captions,
1422 size_t* fewest_closed_captions,
1423 map<Marker, Time>* markers_seen
1426 for (auto i: reel->assets()) {
1427 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1428 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1430 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1431 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1433 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1434 if (i->encryptable() && !file_asset->hash()) {
1435 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1439 if (dcp->standard() == Standard::SMPTE) {
1440 boost::optional<int64_t> duration;
1441 for (auto i: reel->assets()) {
1443 duration = i->actual_duration();
1444 } else if (*duration != i->actual_duration()) {
1445 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1451 if (reel->main_picture()) {
1452 /* Check reel stuff */
1453 auto const frame_rate = reel->main_picture()->frame_rate();
1454 if (frame_rate.denominator != 1 ||
1455 (frame_rate.numerator != 24 &&
1456 frame_rate.numerator != 25 &&
1457 frame_rate.numerator != 30 &&
1458 frame_rate.numerator != 48 &&
1459 frame_rate.numerator != 50 &&
1460 frame_rate.numerator != 60 &&
1461 frame_rate.numerator != 96)) {
1463 VerificationNote::Type::ERROR,
1464 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1465 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1469 if (reel->main_picture()->asset_ref().resolved()) {
1470 verify_main_picture_asset(dcp, reel->main_picture(), start_frame, stage, progress, options, notes);
1471 auto const asset_size = reel->main_picture()->asset()->size();
1472 if (main_picture_active_area) {
1473 if (main_picture_active_area->width > asset_size.width) {
1475 VerificationNote::Type::ERROR,
1476 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1477 String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1481 if (main_picture_active_area->height > asset_size.height) {
1483 VerificationNote::Type::ERROR,
1484 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1485 String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1494 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1495 verify_main_sound_asset(dcp, reel->main_sound(), stage, progress, options, notes, state);
1498 if (reel->main_subtitle()) {
1499 verify_main_subtitle_reel(reel->main_subtitle(), notes);
1500 if (reel->main_subtitle()->asset_ref().resolved()) {
1501 verify_subtitle_asset(reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, xsd_dtd_directory, notes, state);
1503 *have_main_subtitle = true;
1505 *have_no_main_subtitle = true;
1508 for (auto i: reel->closed_captions()) {
1509 verify_closed_caption_reel(i, notes);
1510 if (i->asset_ref().resolved()) {
1511 verify_closed_caption_asset(i->asset(), i->duration(), stage, xsd_dtd_directory, notes);
1515 if (reel->main_markers()) {
1516 for (auto const& i: reel->main_markers()->get()) {
1517 markers_seen->insert(i);
1519 if (reel->main_markers()->entry_point()) {
1520 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_ENTRY_POINT});
1522 if (reel->main_markers()->duration()) {
1523 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_DURATION});
1527 *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1528 *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1536 shared_ptr<const DCP> dcp,
1537 shared_ptr<const CPL> cpl,
1538 function<void (string, optional<boost::filesystem::path>)> stage,
1539 boost::filesystem::path xsd_dtd_directory,
1540 function<void (float)> progress,
1541 VerificationOptions options,
1542 vector<VerificationNote>& notes,
1546 stage("Checking CPL", cpl->file());
1547 validate_xml(cpl->file().get(), xsd_dtd_directory, notes);
1549 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1550 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1553 for (auto const& i: cpl->additional_subtitle_languages()) {
1554 verify_language_tag(i, notes);
1557 if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1558 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1559 * of the approved ones.
1561 auto all = ContentKind::all();
1562 auto name = cpl->content_kind().name();
1563 transform(name.begin(), name.end(), name.begin(), ::tolower);
1564 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1565 if (iter == all.end()) {
1566 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name()});
1570 if (cpl->release_territory()) {
1571 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") {
1572 auto terr = cpl->release_territory().get();
1573 /* Must be a valid region tag, or "001" */
1575 LanguageTag::RegionSubtag test(terr);
1577 if (terr != "001") {
1578 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1584 for (auto version: cpl->content_versions()) {
1585 if (version.label_text.empty()) {
1587 dcp::VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT, cpl->file().get()).set_id(cpl->id())
1593 if (dcp->standard() == Standard::SMPTE) {
1594 if (!cpl->annotation_text()) {
1595 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1596 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1597 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1601 for (auto i: dcp->pkls()) {
1602 /* Check that the CPL's hash corresponds to the PKL */
1603 optional<string> h = i->hash(cpl->id());
1604 auto calculated_cpl_hash = make_digest(ArrayData(*cpl->file()));
1605 if (h && calculated_cpl_hash != *h) {
1607 dcp::VerificationNote(
1608 VerificationNote::Type::ERROR,
1609 VerificationNote::Code::MISMATCHED_CPL_HASHES,
1612 ).set_calculated_hash(calculated_cpl_hash).set_reference_hash(*h)
1616 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1617 optional<string> required_annotation_text;
1618 for (auto j: i->assets()) {
1619 /* See if this is a CPL */
1620 for (auto k: dcp->cpls()) {
1621 if (j->id() == k->id()) {
1622 if (!required_annotation_text) {
1623 /* First CPL we have found; this is the required AnnotationText unless we find another */
1624 required_annotation_text = cpl->content_title_text();
1626 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1627 required_annotation_text = boost::none;
1633 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1634 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1638 /* set to true if any reel has a MainSubtitle */
1639 auto have_main_subtitle = false;
1640 /* set to true if any reel has no MainSubtitle */
1641 auto have_no_main_subtitle = false;
1642 /* fewest number of closed caption assets seen in a reel */
1643 size_t fewest_closed_captions = SIZE_MAX;
1644 /* most number of closed caption assets seen in a reel */
1645 size_t most_closed_captions = 0;
1646 map<Marker, Time> markers_seen;
1648 auto const main_picture_active_area = cpl->main_picture_active_area();
1649 if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1651 VerificationNote::Type::ERROR,
1652 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1653 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1657 if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1659 VerificationNote::Type::ERROR,
1660 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1661 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1667 for (auto reel: cpl->reels()) {
1668 stage("Checking reel", optional<boost::filesystem::path>());
1674 main_picture_active_area,
1681 &have_main_subtitle,
1682 &have_no_main_subtitle,
1683 &most_closed_captions,
1684 &fewest_closed_captions,
1687 frame += reel->duration();
1690 verify_text_details(dcp->standard().get_value_or(dcp::Standard::SMPTE), cpl->reels(), notes);
1692 if (dcp->standard() == Standard::SMPTE) {
1693 if (auto msc = cpl->main_sound_configuration()) {
1694 if (state.audio_channels && msc->channels() != *state.audio_channels) {
1696 VerificationNote::Type::ERROR,
1697 VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION,
1698 String::compose("MainSoundConfiguration has %1 channels but sound assets have %2", msc->channels(), *state.audio_channels),
1704 if (have_main_subtitle && have_no_main_subtitle) {
1705 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1708 if (fewest_closed_captions != most_closed_captions) {
1709 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1712 if (cpl->content_kind() == ContentKind::FEATURE) {
1713 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1714 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1716 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1717 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1721 auto ffoc = markers_seen.find(Marker::FFOC);
1722 if (ffoc == markers_seen.end()) {
1723 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1724 } else if (ffoc->second.e != 1) {
1725 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1728 auto lfoc = markers_seen.find(Marker::LFOC);
1729 if (lfoc == markers_seen.end()) {
1730 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1732 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1733 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1734 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1738 LinesCharactersResult result;
1739 for (auto reel: cpl->reels()) {
1740 if (reel->main_subtitle() && reel->main_subtitle()->asset_ref().resolved()) {
1741 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1745 if (result.line_count_exceeded) {
1746 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1748 if (result.error_length_exceeded) {
1749 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1750 } else if (result.warning_length_exceeded) {
1751 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1754 result = LinesCharactersResult();
1755 for (auto reel: cpl->reels()) {
1756 for (auto i: reel->closed_captions()) {
1758 verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1763 if (result.line_count_exceeded) {
1764 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1766 if (result.error_length_exceeded) {
1767 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1770 if (!cpl->read_composition_metadata()) {
1771 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1772 } else if (!cpl->version_number()) {
1773 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1776 verify_extension_metadata(cpl, notes);
1778 if (cpl->any_encrypted()) {
1779 cxml::Document doc("CompositionPlaylist");
1780 DCP_ASSERT(cpl->file());
1781 doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1782 if (!doc.optional_node_child("Signature")) {
1783 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1793 shared_ptr<const DCP> dcp,
1794 shared_ptr<const PKL> pkl,
1795 boost::filesystem::path xsd_dtd_directory,
1796 vector<VerificationNote>& notes
1799 validate_xml(pkl->file().get(), xsd_dtd_directory, notes);
1801 if (pkl_has_encrypted_assets(dcp, pkl)) {
1802 cxml::Document doc("PackingList");
1803 doc.read_file(dcp::filesystem::fix_long_path(pkl->file().get()));
1804 if (!doc.optional_node_child("Signature")) {
1805 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1809 set<string> uuid_set;
1810 for (auto asset: pkl->assets()) {
1811 if (!uuid_set.insert(asset->id()).second) {
1812 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get()});
1823 shared_ptr<const DCP> dcp,
1824 boost::filesystem::path xsd_dtd_directory,
1825 vector<VerificationNote>& notes
1828 auto asset_map = dcp->asset_map();
1829 DCP_ASSERT(asset_map);
1831 validate_xml(asset_map->file().get(), xsd_dtd_directory, notes);
1833 set<string> uuid_set;
1834 for (auto const& asset: asset_map->assets()) {
1835 if (!uuid_set.insert(asset.id()).second) {
1836 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get()});
1843 vector<VerificationNote>
1845 vector<boost::filesystem::path> directories,
1846 vector<dcp::DecryptedKDM> kdms,
1847 function<void (string, optional<boost::filesystem::path>)> stage,
1848 function<void (float)> progress,
1849 VerificationOptions options,
1850 optional<boost::filesystem::path> xsd_dtd_directory
1853 if (!xsd_dtd_directory) {
1854 xsd_dtd_directory = resources_directory() / "xsd";
1856 *xsd_dtd_directory = filesystem::canonical(*xsd_dtd_directory);
1858 vector<VerificationNote> notes;
1861 vector<shared_ptr<DCP>> dcps;
1862 for (auto i: directories) {
1863 dcps.push_back (make_shared<DCP>(i));
1866 for (auto dcp: dcps) {
1867 stage ("Checking DCP", dcp->directory());
1868 bool carry_on = true;
1870 dcp->read (¬es, true);
1871 } catch (MissingAssetmapError& e) {
1872 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1874 } catch (ReadError& e) {
1875 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1876 } catch (XMLError& e) {
1877 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1878 } catch (MXFFileError& e) {
1879 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1880 } catch (BadURNUUIDError& e) {
1881 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1882 } catch (cxml::Error& e) {
1883 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1890 if (dcp->standard() != Standard::SMPTE) {
1891 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1894 for (auto kdm: kdms) {
1898 for (auto cpl: dcp->cpls()) {
1910 } catch (ReadError& e) {
1911 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1915 for (auto pkl: dcp->pkls()) {
1916 stage("Checking PKL", pkl->file());
1917 verify_pkl(dcp, pkl, *xsd_dtd_directory, notes);
1920 if (dcp->asset_map_file()) {
1921 stage("Checking ASSETMAP", dcp->asset_map_file().get());
1922 verify_assetmap(dcp, *xsd_dtd_directory, notes);
1924 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1933 dcp::note_to_string (VerificationNote note)
1935 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1937 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1938 * not "ClosedCaption assets must have an <EntryPoint> tag."
1940 * It's OK to use XML tag names where they are clear.
1941 * If both ID and filename are available, use only the ID.
1942 * End messages with a full stop.
1943 * Messages should not mention whether or not their errors are a part of Bv2.1.
1945 switch (note.code()) {
1946 case VerificationNote::Code::FAILED_READ:
1947 return *note.note();
1948 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1949 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.note().get(), note.calculated_hash().get());
1950 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1951 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1952 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1953 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());
1954 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1955 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1956 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1957 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());
1958 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1959 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1960 case VerificationNote::Code::EMPTY_ASSET_PATH:
1961 return "The asset map contains an empty asset path.";
1962 case VerificationNote::Code::MISSING_ASSET:
1963 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1964 case VerificationNote::Code::MISMATCHED_STANDARD:
1965 return "The DCP contains both SMPTE and Interop parts.";
1966 case VerificationNote::Code::INVALID_XML:
1967 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1968 case VerificationNote::Code::MISSING_ASSETMAP:
1969 return "No valid ASSETMAP or ASSETMAP.xml was found.";
1970 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1971 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1972 case VerificationNote::Code::INVALID_DURATION:
1973 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1974 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1975 return String::compose(
1976 "Frame %1 (timecode %2) in asset %3 has an instantaneous bit rate that is larger than the limit of 250Mbit/s.",
1978 dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
1979 note.file()->filename()
1981 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1982 return String::compose(
1983 "Frame %1 (timecode %2) in asset %3 has an instantaneous bit rate that is close to the limit of 250Mbit/s.",
1985 dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
1986 note.file()->filename()
1988 case VerificationNote::Code::EXTERNAL_ASSET:
1989 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());
1990 case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1991 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1992 case VerificationNote::Code::INVALID_STANDARD:
1993 return "This DCP does not use the SMPTE standard.";
1994 case VerificationNote::Code::INVALID_LANGUAGE:
1995 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1996 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1997 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1998 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1999 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
2000 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
2001 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
2002 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
2003 return "3D 4K DCPs are not allowed.";
2004 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
2005 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
2006 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
2007 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
2008 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
2009 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());
2010 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
2011 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
2012 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
2013 return "Some subtitle assets have different <Language> tags than others";
2014 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
2015 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
2016 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
2017 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
2018 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
2019 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
2020 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
2021 return "At least one subtitle lasts less than 15 frames.";
2022 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
2023 return "At least one pair of subtitles is separated by less than 2 frames.";
2024 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
2025 return "At least one subtitle extends outside of its reel.";
2026 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
2027 return "There are more than 3 subtitle lines in at least one place in the DCP.";
2028 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
2029 return "There are more than 52 characters in at least one subtitle line.";
2030 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
2031 return "There are more than 79 characters in at least one subtitle line.";
2032 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
2033 return "There are more than 3 closed caption lines in at least one place.";
2034 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
2035 return "There are more than 32 characters in at least one closed caption line.";
2036 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
2037 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
2038 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
2039 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
2040 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
2041 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.note().get());
2042 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
2043 return "All assets in a reel do not have the same duration.";
2044 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
2045 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
2046 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
2047 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
2048 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
2049 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
2050 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
2051 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
2052 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
2053 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
2054 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
2055 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
2056 case VerificationNote::Code::MISSING_HASH:
2057 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
2058 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
2059 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
2060 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
2061 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
2062 case VerificationNote::Code::MISSING_FFOC:
2063 return "There should be a FFOC (first frame of content) marker.";
2064 case VerificationNote::Code::MISSING_LFOC:
2065 return "There should be a LFOC (last frame of content) marker.";
2066 case VerificationNote::Code::INCORRECT_FFOC:
2067 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
2068 case VerificationNote::Code::INCORRECT_LFOC:
2069 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
2070 case VerificationNote::Code::MISSING_CPL_METADATA:
2071 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
2072 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
2073 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
2074 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
2075 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
2076 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
2077 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
2078 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
2079 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
2080 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
2081 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
2082 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
2083 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
2084 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
2085 return "Some assets are encrypted but some are not.";
2086 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
2087 return String::compose(
2088 "Frame %1 (timecode %2) has an invalid JPEG2000 codestream (%3).",
2090 dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
2093 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
2094 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
2095 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
2096 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
2097 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
2098 return "The JPEG2000 tile size is not the same as the image size.";
2099 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
2100 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
2101 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
2102 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
2103 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
2104 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
2105 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
2106 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
2107 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
2108 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
2109 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
2110 return "POC marker found outside main header.";
2111 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
2112 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
2113 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
2114 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
2115 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
2116 return "No TLM marker was found in a JPEG2000 codestream.";
2117 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
2118 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
2119 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
2120 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
2121 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
2123 vector<string> parts;
2124 boost::split (parts, note.note().get(), boost::is_any_of(" "));
2125 DCP_ASSERT (parts.size() == 2);
2126 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]);
2128 case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
2129 return "Some aspect of this DCP could not be checked because it is encrypted.";
2130 case VerificationNote::Code::EMPTY_TEXT:
2131 return "There is an empty <Text> node in a subtitle or closed caption.";
2132 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
2133 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
2134 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
2135 return "Some closed captions are not listed in the order of their vertical position.";
2136 case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
2137 return "There is an <EntryPoint> node inside a <MainMarkers>.";
2138 case VerificationNote::Code::UNEXPECTED_DURATION:
2139 return "There is an <Duration> node inside a <MainMarkers>.";
2140 case VerificationNote::Code::INVALID_CONTENT_KIND:
2141 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
2142 case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
2143 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
2144 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
2145 return String::compose("The PKL %1 has more than one asset with the same ID.", note.note().get());
2146 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
2147 return String::compose("The ASSETMAP %1 has more than one asset with the same ID.", note.note().get());
2148 case VerificationNote::Code::MISSING_SUBTITLE:
2149 return String::compose("The subtitle asset %1 has no subtitles.", note.note().get());
2150 case VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
2151 return String::compose("<IssueDate> has an invalid value: %1", note.note().get());
2152 case VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS:
2153 return String::compose("The sound assets do not all have the same channel count; the first to differ is %1", note.file()->filename());
2154 case VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION:
2155 return String::compose("<MainSoundConfiguration> has an invalid value: %1", note.note().get());
2156 case VerificationNote::Code::MISSING_FONT:
2157 return String::compose("The font file for font ID \"%1\" was not found, or was not referred to in the ASSETMAP.", note.note().get());
2158 case VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE:
2159 return String::compose(
2160 "Frame %1 has an image component that is too large (component %2 is %3 bytes in size).",
2161 note.frame().get(), note.component().get(), note.size().get()
2163 case VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT:
2164 return String::compose("The XML in the subtitle asset %1 has more than one namespace declaration.", note.note().get());
2165 case VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT:
2166 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());
2167 case VerificationNote::Code::MISSING_LOAD_FONT:
2168 return String::compose("The SMPTE subtitle asset %1 has <Text> nodes but no <LoadFont> node", note.id().get());
2169 case VerificationNote::Code::MISMATCHED_ASSET_MAP_ID:
2170 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());
2171 case VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT:
2172 return String::compose("The <LabelText> in a <ContentVersion> in CPL %1 is empty", note.id().get());
2173 case VerificationNote::Code::INVALID_CPL_NAMESPACE:
2174 return String::compose("The namespace %1 in CPL %2 is invalid", note.note().get(), note.file()->filename());
2175 case VerificationNote::Code::MISSING_CPL_CONTENT_VERSION:
2176 return String::compose("The CPL %1 has no <ContentVersion> tag", note.note().get());
2184 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2186 return a.type() == b.type() &&
2187 a.code() == b.code() &&
2188 a.note() == b.note() &&
2189 a.file() == b.file() &&
2190 a.line() == b.line() &&
2191 a.frame() == b.frame() &&
2192 a.component() == b.component() &&
2193 a.size() == b.size() &&
2195 a.other_id() == b.other_id() &&
2196 a.frame_rate() == b.frame_rate() &&
2197 a.reference_hash() == b.reference_hash() &&
2198 a.calculated_hash() == b.calculated_hash();
2203 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2205 if (a.type() != b.type()) {
2206 return a.type() < b.type();
2209 if (a.code() != b.code()) {
2210 return a.code() < b.code();
2213 if (a.note() != b.note()) {
2214 return a.note().get_value_or("") < b.note().get_value_or("");
2217 if (a.file() != b.file()) {
2218 return a.file().get_value_or("") < b.file().get_value_or("");
2221 if (a.line() != b.line()) {
2222 return a.line().get_value_or(0) < b.line().get_value_or(0);
2225 if (a.frame() != b.frame()) {
2226 return a.frame().get_value_or(0) < b.frame().get_value_or(0);
2229 if (a.component() != b.component()) {
2230 return a.component().get_value_or(0) < b.component().get_value_or(0);
2233 if (a.size() != b.size()) {
2234 return a.size().get_value_or(0) < b.size().get_value_or(0);
2237 if (a.id() != b.id()) {
2238 return a.id().get_value_or("") < b.id().get_value_or("");
2241 if (a.other_id() != b.other_id()) {
2242 return a.other_id().get_value_or("") < b.other_id().get_value_or("");
2245 return a.frame_rate().get_value_or(0) != b.frame_rate().get_value_or(0);
2250 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2252 s << note_to_string (note);
2254 s << " [" << note.note().get() << "]";
2257 s << " [" << note.file().get() << "]";
2260 s << " [" << note.line().get() << "]";