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,
1185 struct LinesCharactersResult
1187 bool warning_length_exceeded = false;
1188 bool error_length_exceeded = false;
1189 bool line_count_exceeded = false;
1195 verify_text_lines_and_characters (
1196 shared_ptr<SubtitleAsset> asset,
1199 LinesCharactersResult* result
1205 Event (Time time_, float position_, int characters_)
1207 , position (position_)
1208 , characters (characters_)
1211 Event (Time time_, shared_ptr<Event> start_)
1217 int position; //< position from 0 at top of screen to 100 at bottom
1219 shared_ptr<Event> start;
1222 vector<shared_ptr<Event>> events;
1224 auto position = [](shared_ptr<const SubtitleString> sub) {
1225 switch (sub->v_align()) {
1227 return lrintf(sub->v_position() * 100);
1228 case VAlign::CENTER:
1229 return lrintf((0.5f + sub->v_position()) * 100);
1230 case VAlign::BOTTOM:
1231 return lrintf((1.0f - sub->v_position()) * 100);
1237 for (auto j: asset->subtitles()) {
1238 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1240 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1241 events.push_back(in);
1242 events.push_back(make_shared<Event>(text->out(), in));
1246 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1247 return a->time < b->time;
1250 map<int, int> current;
1251 for (auto i: events) {
1252 if (current.size() > 3) {
1253 result->line_count_exceeded = true;
1255 for (auto j: current) {
1256 if (j.second > warning_length) {
1257 result->warning_length_exceeded = true;
1259 if (j.second > error_length) {
1260 result->error_length_exceeded = true;
1265 /* end of a subtitle */
1266 DCP_ASSERT (current.find(i->start->position) != current.end());
1267 if (current[i->start->position] == i->start->characters) {
1268 current.erase(i->start->position);
1270 current[i->start->position] -= i->start->characters;
1273 /* start of a subtitle */
1274 if (current.find(i->position) == current.end()) {
1275 current[i->position] = i->characters;
1277 current[i->position] += i->characters;
1286 verify_text_details(dcp::Standard standard, vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1288 if (reels.empty()) {
1292 if (reels[0]->main_subtitle() && reels[0]->main_subtitle()->asset_ref().resolved()) {
1293 verify_text_details(standard, reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1294 [](shared_ptr<Reel> reel) {
1295 return static_cast<bool>(reel->main_subtitle());
1297 [](shared_ptr<Reel> reel) {
1298 return reel->main_subtitle()->asset()->raw_xml();
1300 [](shared_ptr<Reel> reel) {
1301 return reel->main_subtitle()->actual_duration();
1303 [](shared_ptr<Reel> reel) {
1304 return reel->main_subtitle()->id();
1309 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1310 verify_text_details(standard, reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1311 [i](shared_ptr<Reel> reel) {
1312 return i < reel->closed_captions().size();
1314 [i](shared_ptr<Reel> reel) {
1315 return reel->closed_captions()[i]->asset()->raw_xml();
1317 [i](shared_ptr<Reel> reel) {
1318 return reel->closed_captions()[i]->actual_duration();
1320 [i](shared_ptr<Reel> reel) {
1321 return reel->closed_captions()[i]->id();
1326 verify_closed_caption_details (reels, notes);
1331 verify_extension_metadata(shared_ptr<const CPL> cpl, vector<VerificationNote>& notes)
1333 DCP_ASSERT (cpl->file());
1334 cxml::Document doc ("CompositionPlaylist");
1335 doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1337 auto missing = false;
1340 if (auto reel_list = doc.node_child("ReelList")) {
1341 auto reels = reel_list->node_children("Reel");
1342 if (!reels.empty()) {
1343 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1344 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1345 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1347 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1348 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1352 if (auto name = extension->optional_node_child("Name")) {
1353 if (name->content() != "Application") {
1354 malformed = "<Name> should be 'Application'";
1357 if (auto property_list = extension->optional_node_child("PropertyList")) {
1358 if (auto property = property_list->optional_node_child("Property")) {
1359 if (auto name = property->optional_node_child("Name")) {
1360 if (name->content() != "DCP Constraints Profile") {
1361 malformed = "<Name> property should be 'DCP Constraints Profile'";
1364 if (auto value = property->optional_node_child("Value")) {
1365 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1366 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1381 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1382 } else if (!malformed.empty()) {
1383 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1389 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1391 vector<string> encrypted;
1392 for (auto i: dcp->cpls()) {
1393 for (auto j: i->reel_file_assets()) {
1394 if (j->asset_ref().resolved()) {
1395 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1396 if (mxf && mxf->encrypted()) {
1397 encrypted.push_back(j->asset_ref().id());
1403 for (auto i: pkl->assets()) {
1404 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1416 shared_ptr<const DCP> dcp,
1417 shared_ptr<const CPL> cpl,
1418 shared_ptr<const Reel> reel,
1419 int64_t start_frame,
1420 optional<dcp::Size> main_picture_active_area,
1421 function<void (string, optional<boost::filesystem::path>)> stage,
1422 boost::filesystem::path xsd_dtd_directory,
1423 function<void (float)> progress,
1424 VerificationOptions options,
1425 vector<VerificationNote>& notes,
1427 bool* have_main_subtitle,
1428 bool* have_no_main_subtitle,
1429 size_t* most_closed_captions,
1430 size_t* fewest_closed_captions,
1431 map<Marker, Time>* markers_seen
1434 for (auto i: reel->assets()) {
1435 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1436 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1438 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1439 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1441 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1442 if (i->encryptable() && !file_asset->hash()) {
1443 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1447 if (dcp->standard() == Standard::SMPTE) {
1448 boost::optional<int64_t> duration;
1449 for (auto i: reel->assets()) {
1451 duration = i->actual_duration();
1452 } else if (*duration != i->actual_duration()) {
1453 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1459 if (reel->main_picture()) {
1460 /* Check reel stuff */
1461 auto const frame_rate = reel->main_picture()->frame_rate();
1462 if (frame_rate.denominator != 1 ||
1463 (frame_rate.numerator != 24 &&
1464 frame_rate.numerator != 25 &&
1465 frame_rate.numerator != 30 &&
1466 frame_rate.numerator != 48 &&
1467 frame_rate.numerator != 50 &&
1468 frame_rate.numerator != 60 &&
1469 frame_rate.numerator != 96)) {
1471 VerificationNote::Type::ERROR,
1472 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1473 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1477 if (reel->main_picture()->asset_ref().resolved()) {
1478 verify_main_picture_asset(dcp, reel->main_picture(), start_frame, stage, progress, options, notes);
1479 auto const asset_size = reel->main_picture()->asset()->size();
1480 if (main_picture_active_area) {
1481 if (main_picture_active_area->width > asset_size.width) {
1483 VerificationNote::Type::ERROR,
1484 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1485 String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1489 if (main_picture_active_area->height > asset_size.height) {
1491 VerificationNote::Type::ERROR,
1492 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1493 String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1502 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1503 verify_main_sound_asset(dcp, reel->main_sound(), stage, progress, options, notes, state);
1506 if (reel->main_subtitle()) {
1507 verify_main_subtitle_reel(reel->main_subtitle(), notes);
1508 if (reel->main_subtitle()->asset_ref().resolved()) {
1509 verify_subtitle_asset(reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, xsd_dtd_directory, notes, state);
1511 *have_main_subtitle = true;
1513 *have_no_main_subtitle = true;
1516 for (auto i: reel->closed_captions()) {
1517 verify_closed_caption_reel(i, notes);
1518 if (i->asset_ref().resolved()) {
1519 verify_closed_caption_asset(i->asset(), i->duration(), stage, xsd_dtd_directory, notes);
1523 if (reel->main_markers()) {
1524 for (auto const& i: reel->main_markers()->get()) {
1525 markers_seen->insert(i);
1527 if (reel->main_markers()->entry_point()) {
1528 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_ENTRY_POINT});
1530 if (reel->main_markers()->duration()) {
1531 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_DURATION});
1535 *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1536 *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1544 shared_ptr<const DCP> dcp,
1545 shared_ptr<const CPL> cpl,
1546 function<void (string, optional<boost::filesystem::path>)> stage,
1547 boost::filesystem::path xsd_dtd_directory,
1548 function<void (float)> progress,
1549 VerificationOptions options,
1550 vector<VerificationNote>& notes,
1554 stage("Checking CPL", cpl->file());
1555 validate_xml(cpl->file().get(), xsd_dtd_directory, notes);
1557 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1558 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1561 for (auto const& i: cpl->additional_subtitle_languages()) {
1562 verify_language_tag(i, notes);
1565 if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1566 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1567 * of the approved ones.
1569 auto all = ContentKind::all();
1570 auto name = cpl->content_kind().name();
1571 transform(name.begin(), name.end(), name.begin(), ::tolower);
1572 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1573 if (iter == all.end()) {
1574 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name()});
1578 if (cpl->release_territory()) {
1579 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") {
1580 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 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1592 for (auto version: cpl->content_versions()) {
1593 if (version.label_text.empty()) {
1595 dcp::VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT, cpl->file().get()).set_id(cpl->id())
1601 if (dcp->standard() == Standard::SMPTE) {
1602 if (!cpl->annotation_text()) {
1603 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1604 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1605 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1609 for (auto i: dcp->pkls()) {
1610 /* Check that the CPL's hash corresponds to the PKL */
1611 optional<string> h = i->hash(cpl->id());
1612 auto calculated_cpl_hash = make_digest(ArrayData(*cpl->file()));
1613 if (h && calculated_cpl_hash != *h) {
1615 dcp::VerificationNote(
1616 VerificationNote::Type::ERROR,
1617 VerificationNote::Code::MISMATCHED_CPL_HASHES,
1620 ).set_calculated_hash(calculated_cpl_hash).set_reference_hash(*h)
1624 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1625 optional<string> required_annotation_text;
1626 for (auto j: i->assets()) {
1627 /* See if this is a CPL */
1628 for (auto k: dcp->cpls()) {
1629 if (j->id() == k->id()) {
1630 if (!required_annotation_text) {
1631 /* First CPL we have found; this is the required AnnotationText unless we find another */
1632 required_annotation_text = cpl->content_title_text();
1634 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1635 required_annotation_text = boost::none;
1641 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1642 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1646 /* set to true if any reel has a MainSubtitle */
1647 auto have_main_subtitle = false;
1648 /* set to true if any reel has no MainSubtitle */
1649 auto have_no_main_subtitle = false;
1650 /* fewest number of closed caption assets seen in a reel */
1651 size_t fewest_closed_captions = SIZE_MAX;
1652 /* most number of closed caption assets seen in a reel */
1653 size_t most_closed_captions = 0;
1654 map<Marker, Time> markers_seen;
1656 auto const main_picture_active_area = cpl->main_picture_active_area();
1657 if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1659 VerificationNote::Type::ERROR,
1660 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1661 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1665 if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1667 VerificationNote::Type::ERROR,
1668 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1669 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1675 for (auto reel: cpl->reels()) {
1676 stage("Checking reel", optional<boost::filesystem::path>());
1682 main_picture_active_area,
1689 &have_main_subtitle,
1690 &have_no_main_subtitle,
1691 &most_closed_captions,
1692 &fewest_closed_captions,
1695 frame += reel->duration();
1698 verify_text_details(dcp->standard().get_value_or(dcp::Standard::SMPTE), cpl->reels(), notes);
1700 if (dcp->standard() == Standard::SMPTE) {
1701 if (auto msc = cpl->main_sound_configuration()) {
1702 if (state.audio_channels && msc->channels() != *state.audio_channels) {
1704 VerificationNote::Type::ERROR,
1705 VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION,
1706 String::compose("MainSoundConfiguration has %1 channels but sound assets have %2", msc->channels(), *state.audio_channels),
1712 if (have_main_subtitle && have_no_main_subtitle) {
1713 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1716 if (fewest_closed_captions != most_closed_captions) {
1717 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1720 if (cpl->content_kind() == ContentKind::FEATURE) {
1721 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1722 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1724 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1725 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1729 auto ffoc = markers_seen.find(Marker::FFOC);
1730 if (ffoc == markers_seen.end()) {
1731 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1732 } else if (ffoc->second.e != 1) {
1733 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1736 auto lfoc = markers_seen.find(Marker::LFOC);
1737 if (lfoc == markers_seen.end()) {
1738 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1740 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1741 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1742 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1746 LinesCharactersResult result;
1747 for (auto reel: cpl->reels()) {
1748 if (reel->main_subtitle() && reel->main_subtitle()->asset_ref().resolved()) {
1749 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1753 if (result.line_count_exceeded) {
1754 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1756 if (result.error_length_exceeded) {
1757 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1758 } else if (result.warning_length_exceeded) {
1759 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1762 result = LinesCharactersResult();
1763 for (auto reel: cpl->reels()) {
1764 for (auto i: reel->closed_captions()) {
1766 verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1771 if (result.line_count_exceeded) {
1772 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1774 if (result.error_length_exceeded) {
1775 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1778 if (!cpl->read_composition_metadata()) {
1779 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1780 } else if (!cpl->version_number()) {
1781 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1784 verify_extension_metadata(cpl, notes);
1786 if (cpl->any_encrypted()) {
1787 cxml::Document doc("CompositionPlaylist");
1788 DCP_ASSERT(cpl->file());
1789 doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1790 if (!doc.optional_node_child("Signature")) {
1791 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1801 shared_ptr<const DCP> dcp,
1802 shared_ptr<const PKL> pkl,
1803 boost::filesystem::path xsd_dtd_directory,
1804 vector<VerificationNote>& notes
1807 validate_xml(pkl->file().get(), xsd_dtd_directory, notes);
1809 if (pkl_has_encrypted_assets(dcp, pkl)) {
1810 cxml::Document doc("PackingList");
1811 doc.read_file(dcp::filesystem::fix_long_path(pkl->file().get()));
1812 if (!doc.optional_node_child("Signature")) {
1813 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1817 set<string> uuid_set;
1818 for (auto asset: pkl->assets()) {
1819 if (!uuid_set.insert(asset->id()).second) {
1820 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get()});
1831 shared_ptr<const DCP> dcp,
1832 boost::filesystem::path xsd_dtd_directory,
1833 vector<VerificationNote>& notes
1836 auto asset_map = dcp->asset_map();
1837 DCP_ASSERT(asset_map);
1839 validate_xml(asset_map->file().get(), xsd_dtd_directory, notes);
1841 set<string> uuid_set;
1842 for (auto const& asset: asset_map->assets()) {
1843 if (!uuid_set.insert(asset.id()).second) {
1844 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get()});
1851 vector<VerificationNote>
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;
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());
1876 bool carry_on = true;
1878 dcp->read (¬es, true);
1879 } catch (MissingAssetmapError& e) {
1880 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1882 } catch (ReadError& e) {
1883 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1884 } catch (XMLError& e) {
1885 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1886 } catch (MXFFileError& e) {
1887 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1888 } catch (BadURNUUIDError& e) {
1889 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1890 } catch (cxml::Error& e) {
1891 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1898 if (dcp->standard() != Standard::SMPTE) {
1899 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1902 for (auto kdm: kdms) {
1906 for (auto cpl: dcp->cpls()) {
1918 } catch (ReadError& e) {
1919 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1923 for (auto pkl: dcp->pkls()) {
1924 stage("Checking PKL", pkl->file());
1925 verify_pkl(dcp, pkl, *xsd_dtd_directory, notes);
1928 if (dcp->asset_map_file()) {
1929 stage("Checking ASSETMAP", dcp->asset_map_file().get());
1930 verify_assetmap(dcp, *xsd_dtd_directory, notes);
1932 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1941 dcp::note_to_string (VerificationNote note)
1943 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1945 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1946 * not "ClosedCaption assets must have an <EntryPoint> tag."
1948 * It's OK to use XML tag names where they are clear.
1949 * If both ID and filename are available, use only the ID.
1950 * End messages with a full stop.
1951 * Messages should not mention whether or not their errors are a part of Bv2.1.
1953 switch (note.code()) {
1954 case VerificationNote::Code::FAILED_READ:
1955 return *note.note();
1956 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1957 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());
1958 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1959 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1960 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1961 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());
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::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1983 return String::compose(
1984 "Frame %1 (timecode %2) in asset %3 has an instantaneous bit rate that is larger than the limit of 250Mbit/s.",
1986 dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
1987 note.file()->filename()
1989 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1990 return String::compose(
1991 "Frame %1 (timecode %2) in asset %3 has an instantaneous bit rate that is close to the limit of 250Mbit/s.",
1993 dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
1994 note.file()->filename()
1996 case VerificationNote::Code::EXTERNAL_ASSET:
1997 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());
1998 case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1999 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
2000 case VerificationNote::Code::INVALID_STANDARD:
2001 return "This DCP does not use the SMPTE standard.";
2002 case VerificationNote::Code::INVALID_LANGUAGE:
2003 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
2004 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
2005 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
2006 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
2007 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
2008 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
2009 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
2010 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
2011 return "3D 4K DCPs are not allowed.";
2012 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
2013 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
2014 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
2015 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
2016 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
2017 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());
2018 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
2019 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
2020 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
2021 return "Some subtitle assets have different <Language> tags than others";
2022 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
2023 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
2024 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
2025 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
2026 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
2027 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
2028 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
2029 return "At least one subtitle lasts less than 15 frames.";
2030 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
2031 return "At least one pair of subtitles is separated by less than 2 frames.";
2032 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
2033 return "At least one subtitle extends outside of its reel.";
2034 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
2035 return "There are more than 3 subtitle lines in at least one place in the DCP.";
2036 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
2037 return "There are more than 52 characters in at least one subtitle line.";
2038 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
2039 return "There are more than 79 characters in at least one subtitle line.";
2040 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
2041 return "There are more than 3 closed caption lines in at least one place.";
2042 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
2043 return "There are more than 32 characters in at least one closed caption line.";
2044 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
2045 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
2046 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
2047 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
2048 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
2049 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.note().get());
2050 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
2051 return "All assets in a reel do not have the same duration.";
2052 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
2053 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
2054 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
2055 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
2056 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
2057 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
2058 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
2059 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
2060 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
2061 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
2062 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
2063 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
2064 case VerificationNote::Code::MISSING_HASH:
2065 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
2066 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
2067 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
2068 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
2069 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
2070 case VerificationNote::Code::MISSING_FFOC:
2071 return "There should be a FFOC (first frame of content) marker.";
2072 case VerificationNote::Code::MISSING_LFOC:
2073 return "There should be a LFOC (last frame of content) marker.";
2074 case VerificationNote::Code::INCORRECT_FFOC:
2075 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
2076 case VerificationNote::Code::INCORRECT_LFOC:
2077 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
2078 case VerificationNote::Code::MISSING_CPL_METADATA:
2079 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
2080 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
2081 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
2082 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
2083 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
2084 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
2085 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
2086 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
2087 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
2088 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
2089 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
2090 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
2091 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
2092 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
2093 return "Some assets are encrypted but some are not.";
2094 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
2095 return String::compose(
2096 "Frame %1 (timecode %2) has an invalid JPEG2000 codestream (%3).",
2098 dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
2101 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
2102 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
2103 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
2104 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
2105 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
2106 return "The JPEG2000 tile size is not the same as the image size.";
2107 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
2108 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
2109 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
2110 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
2111 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
2112 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
2113 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
2114 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
2115 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
2116 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
2117 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
2118 return "POC marker found outside main header.";
2119 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
2120 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
2121 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
2122 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
2123 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
2124 return "No TLM marker was found in a JPEG2000 codestream.";
2125 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
2126 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
2127 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
2128 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
2129 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
2131 vector<string> parts;
2132 boost::split (parts, note.note().get(), boost::is_any_of(" "));
2133 DCP_ASSERT (parts.size() == 2);
2134 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]);
2136 case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
2137 return "Some aspect of this DCP could not be checked because it is encrypted.";
2138 case VerificationNote::Code::EMPTY_TEXT:
2139 return "There is an empty <Text> node in a subtitle or closed caption.";
2140 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
2141 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
2142 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
2143 return "Some closed captions are not listed in the order of their vertical position.";
2144 case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
2145 return "There is an <EntryPoint> node inside a <MainMarkers>.";
2146 case VerificationNote::Code::UNEXPECTED_DURATION:
2147 return "There is an <Duration> node inside a <MainMarkers>.";
2148 case VerificationNote::Code::INVALID_CONTENT_KIND:
2149 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
2150 case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
2151 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
2152 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
2153 return String::compose("The PKL %1 has more than one asset with the same ID.", note.note().get());
2154 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
2155 return String::compose("The ASSETMAP %1 has more than one asset with the same ID.", note.note().get());
2156 case VerificationNote::Code::MISSING_SUBTITLE:
2157 return String::compose("The subtitle asset %1 has no subtitles.", note.note().get());
2158 case VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
2159 return String::compose("<IssueDate> has an invalid value: %1", note.note().get());
2160 case VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS:
2161 return String::compose("The sound assets do not all have the same channel count; the first to differ is %1", note.file()->filename());
2162 case VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION:
2163 return String::compose("<MainSoundConfiguration> has an invalid value: %1", note.note().get());
2164 case VerificationNote::Code::MISSING_FONT:
2165 return String::compose("The font file for font ID \"%1\" was not found, or was not referred to in the ASSETMAP.", note.note().get());
2166 case VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE:
2167 return String::compose(
2168 "Frame %1 has an image component that is too large (component %2 is %3 bytes in size).",
2169 note.frame().get(), note.component().get(), note.size().get()
2171 case VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT:
2172 return String::compose("The XML in the subtitle asset %1 has more than one namespace declaration.", note.note().get());
2173 case VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT:
2174 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());
2175 case VerificationNote::Code::MISSING_LOAD_FONT:
2176 return String::compose("The SMPTE subtitle asset %1 has <Text> nodes but no <LoadFont> node", note.id().get());
2177 case VerificationNote::Code::MISMATCHED_ASSET_MAP_ID:
2178 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());
2179 case VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT:
2180 return String::compose("The <LabelText> in a <ContentVersion> in CPL %1 is empty", note.id().get());
2188 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2190 return a.type() == b.type() &&
2191 a.code() == b.code() &&
2192 a.note() == b.note() &&
2193 a.file() == b.file() &&
2194 a.line() == b.line() &&
2195 a.frame() == b.frame() &&
2196 a.component() == b.component() &&
2197 a.size() == b.size() &&
2199 a.other_id() == b.other_id() &&
2200 a.frame_rate() == b.frame_rate() &&
2201 a.reference_hash() == b.reference_hash() &&
2202 a.calculated_hash() == b.calculated_hash();
2207 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2209 if (a.type() != b.type()) {
2210 return a.type() < b.type();
2213 if (a.code() != b.code()) {
2214 return a.code() < b.code();
2217 if (a.note() != b.note()) {
2218 return a.note().get_value_or("") < b.note().get_value_or("");
2221 if (a.file() != b.file()) {
2222 return a.file().get_value_or("") < b.file().get_value_or("");
2225 if (a.line() != b.line()) {
2226 return a.line().get_value_or(0) < b.line().get_value_or(0);
2229 if (a.frame() != b.frame()) {
2230 return a.frame().get_value_or(0) < b.frame().get_value_or(0);
2233 if (a.component() != b.component()) {
2234 return a.component().get_value_or(0) < b.component().get_value_or(0);
2237 if (a.size() != b.size()) {
2238 return a.size().get_value_or(0) < b.size().get_value_or(0);
2241 if (a.id() != b.id()) {
2242 return a.id().get_value_or("") < b.id().get_value_or("");
2245 if (a.other_id() != b.other_id()) {
2246 return a.other_id().get_value_or("") < b.other_id().get_value_or("");
2249 return a.frame_rate().get_value_or(0) != b.frame_rate().get_value_or(0);
2254 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2256 s << note_to_string (note);
2258 s << " [" << note.note().get() << "]";
2261 s << " [" << note.file().get() << "]";
2264 s << " [" << note.line().get() << "]";