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
388 verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelFileAsset> reel_file_asset, function<void (float)> progress)
390 /* When reading the DCP the hash will have been set to the one from the PKL/CPL.
391 * We want to calculate the hash of the actual file contents here, so that we
392 * can check it. unset_hash() means that this calculation will happen on the
395 reel_file_asset->asset_ref()->unset_hash();
396 auto const actual_hash = reel_file_asset->asset_ref()->hash(progress);
398 auto pkls = dcp->pkls();
399 /* We've read this DCP in so it must have at least one PKL */
400 DCP_ASSERT (!pkls.empty());
402 auto asset = reel_file_asset->asset_ref().asset();
404 optional<string> pkl_hash;
406 pkl_hash = i->hash (reel_file_asset->asset_ref()->id());
412 DCP_ASSERT (pkl_hash);
414 auto cpl_hash = reel_file_asset->hash();
415 if (cpl_hash && *cpl_hash != *pkl_hash) {
416 return VerifyAssetResult::CPL_PKL_DIFFER;
419 if (actual_hash != *pkl_hash) {
420 return VerifyAssetResult::BAD;
423 return VerifyAssetResult::GOOD;
428 verify_language_tag (string tag, vector<VerificationNote>& notes)
431 LanguageTag test (tag);
432 } catch (LanguageTagError &) {
433 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, tag});
439 verify_picture_asset (shared_ptr<const ReelFileAsset> reel_file_asset, boost::filesystem::path file, vector<VerificationNote>& notes, function<void (float)> progress)
441 int biggest_frame = 0;
442 auto asset = dynamic_pointer_cast<PictureAsset>(reel_file_asset->asset_ref().asset());
443 auto const duration = asset->intrinsic_duration ();
445 auto check_and_add = [¬es](vector<VerificationNote> const& j2k_notes) {
446 for (auto i: j2k_notes) {
447 if (find(notes.begin(), notes.end(), i) == notes.end()) {
453 if (auto mono_asset = dynamic_pointer_cast<MonoPictureAsset>(reel_file_asset->asset_ref().asset())) {
454 auto reader = mono_asset->start_read ();
455 for (int64_t i = 0; i < duration; ++i) {
456 auto frame = reader->get_frame (i);
457 biggest_frame = max(biggest_frame, frame->size());
458 if (!mono_asset->encrypted() || mono_asset->key()) {
459 vector<VerificationNote> j2k_notes;
460 verify_j2k(frame, i, mono_asset->frame_rate().numerator, j2k_notes);
461 check_and_add (j2k_notes);
463 progress (float(i) / duration);
465 } else if (auto stereo_asset = dynamic_pointer_cast<StereoPictureAsset>(asset)) {
466 auto reader = stereo_asset->start_read ();
467 for (int64_t i = 0; i < duration; ++i) {
468 auto frame = reader->get_frame (i);
469 biggest_frame = max(biggest_frame, max(frame->left()->size(), frame->right()->size()));
470 if (!stereo_asset->encrypted() || stereo_asset->key()) {
471 vector<VerificationNote> j2k_notes;
472 verify_j2k(frame->left(), i, stereo_asset->frame_rate().numerator, j2k_notes);
473 verify_j2k(frame->right(), i, stereo_asset->frame_rate().numerator, j2k_notes);
474 check_and_add (j2k_notes);
476 progress (float(i) / duration);
481 static const int max_frame = rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
482 static const int risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
483 if (biggest_frame > max_frame) {
485 VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
487 } else if (biggest_frame > risky_frame) {
489 VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
496 verify_main_picture_asset (
497 shared_ptr<const DCP> dcp,
498 shared_ptr<const ReelPictureAsset> reel_asset,
499 function<void (string, optional<boost::filesystem::path>)> stage,
500 function<void (float)> progress,
501 VerificationOptions options,
502 vector<VerificationNote>& notes
505 auto asset = reel_asset->asset();
506 auto const file = *asset->file();
508 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
509 stage ("Checking picture asset hash", file);
510 auto const r = verify_asset (dcp, reel_asset, progress);
512 case VerifyAssetResult::BAD:
514 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file
517 case VerifyAssetResult::CPL_PKL_DIFFER:
519 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file
527 stage ("Checking picture frame sizes", asset->file());
528 verify_picture_asset (reel_asset, file, notes, progress);
530 /* Only flat/scope allowed by Bv2.1 */
532 asset->size() != Size(2048, 858) &&
533 asset->size() != Size(1998, 1080) &&
534 asset->size() != Size(4096, 1716) &&
535 asset->size() != Size(3996, 2160)) {
537 VerificationNote::Type::BV21_ERROR,
538 VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS,
539 String::compose("%1x%2", asset->size().width, asset->size().height),
544 /* Only 24, 25, 48fps allowed for 2K */
546 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
547 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
550 VerificationNote::Type::BV21_ERROR,
551 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
552 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
557 if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
558 /* Only 24fps allowed for 4K */
559 if (asset->edit_rate() != Fraction(24, 1)) {
561 VerificationNote::Type::BV21_ERROR,
562 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
563 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
568 /* Only 2D allowed for 4K */
569 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
571 VerificationNote::Type::BV21_ERROR,
572 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
573 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
585 boost::optional<string> subtitle_language;
586 boost::optional<int> audio_channels;
591 verify_main_sound_asset (
592 shared_ptr<const DCP> dcp,
593 shared_ptr<const ReelSoundAsset> reel_asset,
594 function<void (string, optional<boost::filesystem::path>)> stage,
595 function<void (float)> progress,
596 VerificationOptions options,
597 vector<VerificationNote>& notes,
601 auto asset = reel_asset->asset();
602 auto const file = *asset->file();
604 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
605 stage("Checking sound asset hash", file);
606 auto const r = verify_asset (dcp, reel_asset, progress);
608 case VerifyAssetResult::BAD:
609 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, file});
611 case VerifyAssetResult::CPL_PKL_DIFFER:
612 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, file});
619 if (!state.audio_channels) {
620 state.audio_channels = asset->channels();
621 } else if (*state.audio_channels != asset->channels()) {
622 notes.push_back({ VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS, file });
625 stage ("Checking sound asset metadata", file);
627 if (auto lang = asset->language()) {
628 verify_language_tag (*lang, notes);
630 if (asset->sampling_rate() != 48000) {
631 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), file});
637 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
639 /* XXX: is Language compulsory? */
640 if (reel_asset->language()) {
641 verify_language_tag (*reel_asset->language(), notes);
644 if (!reel_asset->entry_point()) {
645 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id() });
646 } else if (reel_asset->entry_point().get()) {
647 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
653 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
655 /* XXX: is Language compulsory? */
656 if (reel_asset->language()) {
657 verify_language_tag (*reel_asset->language(), notes);
660 if (!reel_asset->entry_point()) {
661 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
662 } else if (reel_asset->entry_point().get()) {
663 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
668 /** Verify stuff that is common to both subtitles and closed captions */
670 verify_smpte_timed_text_asset (
671 shared_ptr<const SMPTESubtitleAsset> asset,
672 optional<int64_t> reel_asset_duration,
673 vector<VerificationNote>& notes
676 if (asset->language()) {
677 verify_language_tag (*asset->language(), notes);
679 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
682 auto const size = filesystem::file_size(asset->file().get());
683 if (size > 115 * 1024 * 1024) {
685 { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file() }
689 /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
690 * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
692 auto fonts = asset->font_data ();
694 for (auto i: fonts) {
695 total_size += i.second.size();
697 if (total_size > 10 * 1024 * 1024) {
698 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
701 if (!asset->start_time()) {
702 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get() });
703 } else if (asset->start_time() != Time()) {
704 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() });
707 if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
710 VerificationNote::Type::BV21_ERROR,
711 VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
712 String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
719 /** Verify Interop subtitle / CCAP stuff */
721 verify_interop_text_asset(shared_ptr<const InteropSubtitleAsset> asset, vector<VerificationNote>& notes)
723 if (asset->subtitles().empty()) {
724 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_SUBTITLE, asset->id(), asset->file().get() });
726 auto const unresolved = asset->unresolved_fonts();
727 if (!unresolved.empty()) {
728 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_FONT, unresolved.front() });
733 /** Verify SMPTE subtitle-only stuff */
735 verify_smpte_subtitle_asset (
736 shared_ptr<const SMPTESubtitleAsset> asset,
737 vector<VerificationNote>& notes,
741 if (asset->language()) {
742 if (!state.subtitle_language) {
743 state.subtitle_language = *asset->language();
744 } else if (state.subtitle_language != *asset->language()) {
745 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
749 DCP_ASSERT (asset->resource_id());
750 auto xml_id = asset->xml_id();
752 if (asset->resource_id().get() != xml_id) {
753 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
756 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
757 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
760 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
763 if (asset->raw_xml()) {
764 /* Deluxe require this in their QC even if it seems never to be mentioned in any standard */
765 cxml::Document doc("SubtitleReel");
766 doc.read_string(*asset->raw_xml());
767 auto issue_date = doc.string_child("IssueDate");
768 std::regex reg("^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d$");
769 if (!std::regex_match(issue_date, reg)) {
770 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, issue_date});
776 /** Verify all subtitle stuff */
778 verify_subtitle_asset (
779 shared_ptr<const SubtitleAsset> asset,
780 optional<int64_t> reel_asset_duration,
781 function<void (string, optional<boost::filesystem::path>)> stage,
782 boost::filesystem::path xsd_dtd_directory,
783 vector<VerificationNote>& notes,
787 stage ("Checking subtitle XML", asset->file());
788 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
789 * gets passed through libdcp which may clean up and therefore hide errors.
791 if (asset->raw_xml()) {
792 validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
794 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
797 auto namespace_count = [](shared_ptr<const SubtitleAsset> asset, string root_node) {
798 cxml::Document doc(root_node);
799 doc.read_string(asset->raw_xml().get());
800 auto root = dynamic_cast<xmlpp::Element*>(doc.node())->cobj();
802 for (auto ns = root->nsDef; ns != nullptr; ns = ns->next) {
808 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
810 verify_interop_text_asset(interop, notes);
811 if (namespace_count(asset, "DCSubtitle") > 1) {
812 notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id() });
816 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
818 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
819 verify_smpte_subtitle_asset (smpte, notes, state);
820 /* This asset may be encrypted and in that case we'll have no raw_xml() */
821 if (asset->raw_xml() && namespace_count(asset, "SubtitleReel") > 1) {
822 notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id()});
828 /** Verify all closed caption stuff */
830 verify_closed_caption_asset (
831 shared_ptr<const SubtitleAsset> asset,
832 optional<int64_t> reel_asset_duration,
833 function<void (string, optional<boost::filesystem::path>)> stage,
834 boost::filesystem::path xsd_dtd_directory,
835 vector<VerificationNote>& notes
838 stage ("Checking closed caption XML", asset->file());
839 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
840 * gets passed through libdcp which may clean up and therefore hide errors.
842 auto raw_xml = asset->raw_xml();
844 validate_xml (*raw_xml, xsd_dtd_directory, notes);
845 if (raw_xml->size() > 256 * 1024) {
846 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
849 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
852 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
854 verify_interop_text_asset(interop, notes);
857 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
859 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
864 /** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes etc. */
867 verify_text_details (
868 dcp::Standard standard,
869 vector<shared_ptr<Reel>> reels,
871 vector<VerificationNote>& notes,
872 std::function<bool (shared_ptr<Reel>)> check,
873 std::function<optional<string> (shared_ptr<Reel>)> xml,
874 std::function<int64_t (shared_ptr<Reel>)> duration,
875 std::function<std::string (shared_ptr<Reel>)> id
878 /* end of last subtitle (in editable units) */
879 optional<int64_t> last_out;
880 auto too_short = false;
881 auto too_close = false;
882 auto too_early = false;
883 auto reel_overlap = false;
884 auto empty_text = false;
885 /* current reel start time (in editable units) */
886 int64_t reel_offset = 0;
887 optional<string> missing_load_font_id;
889 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool, bool&, vector<string>&)> parse;
891 parse = [&parse, &last_out, &too_short, &too_close, &too_early, &empty_text, &reel_offset, &missing_load_font_id](
892 cxml::ConstNodePtr node,
894 optional<Time> start_time,
898 vector<string>& font_ids
900 if (node->name() == "Subtitle") {
901 Time in (node->string_attribute("TimeIn"), tcr);
905 Time out (node->string_attribute("TimeOut"), tcr);
909 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
912 auto length = out - in;
913 if (length.as_editable_units_ceil(er) < 15) {
917 /* XXX: this feels dubious - is it really what Bv2.1 means? */
918 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
919 if (distance >= 0 && distance < 2) {
923 last_out = reel_offset + out.as_editable_units_floor(er);
924 } else if (node->name() == "Text") {
925 std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
926 if (!node->content().empty()) {
929 for (auto i: node->node_children()) {
930 if (node_has_content(i)) {
936 if (!node_has_content(node)) {
940 } else if (node->name() == "LoadFont") {
941 if (auto const id = node->optional_string_attribute("Id")) {
942 font_ids.push_back(*id);
943 } else if (auto const id = node->optional_string_attribute("ID")) {
944 font_ids.push_back(*id);
946 } else if (node->name() == "Font") {
947 if (auto const font_id = node->optional_string_attribute("Id")) {
948 if (std::find_if(font_ids.begin(), font_ids.end(), [font_id](string const& id) { return id == font_id; }) == font_ids.end()) {
949 missing_load_font_id = font_id;
953 for (auto i: node->node_children()) {
954 parse(i, tcr, start_time, er, first_reel, has_text, font_ids);
958 for (auto i = 0U; i < reels.size(); ++i) {
959 if (!check(reels[i])) {
963 auto reel_xml = xml(reels[i]);
965 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
969 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
970 * read in by libdcp's parser.
973 shared_ptr<cxml::Document> doc;
975 optional<Time> start_time;
977 case dcp::Standard::INTEROP:
978 doc = make_shared<cxml::Document>("DCSubtitle");
979 doc->read_string (*reel_xml);
981 case dcp::Standard::SMPTE:
982 doc = make_shared<cxml::Document>("SubtitleReel");
983 doc->read_string (*reel_xml);
984 tcr = doc->number_child<int>("TimeCodeRate");
985 if (auto start_time_string = doc->optional_string_child("StartTime")) {
986 start_time = Time(*start_time_string, tcr);
990 bool has_text = false;
991 vector<string> font_ids;
992 parse(doc, tcr, start_time, edit_rate, i == 0, has_text, font_ids);
993 auto end = reel_offset + duration(reels[i]);
994 if (last_out && *last_out > end) {
999 if (standard == dcp::Standard::SMPTE && has_text && font_ids.empty()) {
1000 notes.push_back(dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT).set_id(id(reels[i])));
1004 if (last_out && *last_out > reel_offset) {
1005 reel_overlap = true;
1010 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
1016 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
1022 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
1028 VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
1034 VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_TEXT
1038 if (missing_load_font_id) {
1039 notes.push_back(dcp::VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT).set_id(*missing_load_font_id));
1046 verify_closed_caption_details (
1047 vector<shared_ptr<Reel>> reels,
1048 vector<VerificationNote>& notes
1051 std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
1052 find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
1053 for (auto i: node->node_children()) {
1054 if (i->name() == "Text") {
1055 text_or_image.push_back (i);
1057 find_text_or_image (i, text_or_image);
1062 auto mismatched_valign = false;
1063 auto incorrect_order = false;
1065 std::function<void (cxml::ConstNodePtr)> parse;
1066 parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
1067 if (node->name() == "Subtitle") {
1068 vector<cxml::ConstNodePtr> text_or_image;
1069 find_text_or_image (node, text_or_image);
1070 optional<string> last_valign;
1071 optional<float> last_vpos;
1072 for (auto i: text_or_image) {
1073 auto valign = i->optional_string_attribute("VAlign");
1075 valign = i->optional_string_attribute("Valign").get_value_or("center");
1077 auto vpos = i->optional_number_attribute<float>("VPosition");
1079 vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
1083 if (*last_valign != valign) {
1084 mismatched_valign = true;
1087 last_valign = valign;
1089 if (!mismatched_valign) {
1091 if (*last_valign == "top" || *last_valign == "center") {
1092 if (*vpos < *last_vpos) {
1093 incorrect_order = true;
1096 if (*vpos > *last_vpos) {
1097 incorrect_order = true;
1106 for (auto i: node->node_children()) {
1111 for (auto reel: reels) {
1112 for (auto ccap: reel->closed_captions()) {
1113 auto reel_xml = ccap->asset()->raw_xml();
1115 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
1119 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1120 * read in by libdcp's parser.
1123 shared_ptr<cxml::Document> doc;
1125 optional<Time> start_time;
1127 doc = make_shared<cxml::Document>("SubtitleReel");
1128 doc->read_string (*reel_xml);
1130 doc = make_shared<cxml::Document>("DCSubtitle");
1131 doc->read_string (*reel_xml);
1137 if (mismatched_valign) {
1139 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN,
1143 if (incorrect_order) {
1145 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING,
1151 struct LinesCharactersResult
1153 bool warning_length_exceeded = false;
1154 bool error_length_exceeded = false;
1155 bool line_count_exceeded = false;
1161 verify_text_lines_and_characters (
1162 shared_ptr<SubtitleAsset> asset,
1165 LinesCharactersResult* result
1171 Event (Time time_, float position_, int characters_)
1173 , position (position_)
1174 , characters (characters_)
1177 Event (Time time_, shared_ptr<Event> start_)
1183 int position; //< position from 0 at top of screen to 100 at bottom
1185 shared_ptr<Event> start;
1188 vector<shared_ptr<Event>> events;
1190 auto position = [](shared_ptr<const SubtitleString> sub) {
1191 switch (sub->v_align()) {
1193 return lrintf(sub->v_position() * 100);
1194 case VAlign::CENTER:
1195 return lrintf((0.5f + sub->v_position()) * 100);
1196 case VAlign::BOTTOM:
1197 return lrintf((1.0f - sub->v_position()) * 100);
1203 for (auto j: asset->subtitles()) {
1204 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1206 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1207 events.push_back(in);
1208 events.push_back(make_shared<Event>(text->out(), in));
1212 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1213 return a->time < b->time;
1216 map<int, int> current;
1217 for (auto i: events) {
1218 if (current.size() > 3) {
1219 result->line_count_exceeded = true;
1221 for (auto j: current) {
1222 if (j.second > warning_length) {
1223 result->warning_length_exceeded = true;
1225 if (j.second > error_length) {
1226 result->error_length_exceeded = true;
1231 /* end of a subtitle */
1232 DCP_ASSERT (current.find(i->start->position) != current.end());
1233 if (current[i->start->position] == i->start->characters) {
1234 current.erase(i->start->position);
1236 current[i->start->position] -= i->start->characters;
1239 /* start of a subtitle */
1240 if (current.find(i->position) == current.end()) {
1241 current[i->position] = i->characters;
1243 current[i->position] += i->characters;
1252 verify_text_details(dcp::Standard standard, vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1254 if (reels.empty()) {
1258 if (reels[0]->main_subtitle()) {
1259 verify_text_details(standard, reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1260 [](shared_ptr<Reel> reel) {
1261 return static_cast<bool>(reel->main_subtitle());
1263 [](shared_ptr<Reel> reel) {
1264 return reel->main_subtitle()->asset()->raw_xml();
1266 [](shared_ptr<Reel> reel) {
1267 return reel->main_subtitle()->actual_duration();
1269 [](shared_ptr<Reel> reel) {
1270 return reel->main_subtitle()->id();
1275 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1276 verify_text_details(standard, reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1277 [i](shared_ptr<Reel> reel) {
1278 return i < reel->closed_captions().size();
1280 [i](shared_ptr<Reel> reel) {
1281 return reel->closed_captions()[i]->asset()->raw_xml();
1283 [i](shared_ptr<Reel> reel) {
1284 return reel->closed_captions()[i]->actual_duration();
1286 [i](shared_ptr<Reel> reel) {
1287 return reel->closed_captions()[i]->id();
1292 verify_closed_caption_details (reels, notes);
1297 verify_extension_metadata(shared_ptr<const CPL> cpl, vector<VerificationNote>& notes)
1299 DCP_ASSERT (cpl->file());
1300 cxml::Document doc ("CompositionPlaylist");
1301 doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1303 auto missing = false;
1306 if (auto reel_list = doc.node_child("ReelList")) {
1307 auto reels = reel_list->node_children("Reel");
1308 if (!reels.empty()) {
1309 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1310 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1311 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1313 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1314 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1318 if (auto name = extension->optional_node_child("Name")) {
1319 if (name->content() != "Application") {
1320 malformed = "<Name> should be 'Application'";
1323 if (auto property_list = extension->optional_node_child("PropertyList")) {
1324 if (auto property = property_list->optional_node_child("Property")) {
1325 if (auto name = property->optional_node_child("Name")) {
1326 if (name->content() != "DCP Constraints Profile") {
1327 malformed = "<Name> property should be 'DCP Constraints Profile'";
1330 if (auto value = property->optional_node_child("Value")) {
1331 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1332 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1347 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1348 } else if (!malformed.empty()) {
1349 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1355 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1357 vector<string> encrypted;
1358 for (auto i: dcp->cpls()) {
1359 for (auto j: i->reel_file_assets()) {
1360 if (j->asset_ref().resolved()) {
1361 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1362 if (mxf && mxf->encrypted()) {
1363 encrypted.push_back(j->asset_ref().id());
1369 for (auto i: pkl->assets()) {
1370 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1382 shared_ptr<const DCP> dcp,
1383 shared_ptr<const CPL> cpl,
1384 shared_ptr<const Reel> reel,
1385 optional<dcp::Size> main_picture_active_area,
1386 function<void (string, optional<boost::filesystem::path>)> stage,
1387 boost::filesystem::path xsd_dtd_directory,
1388 function<void (float)> progress,
1389 VerificationOptions options,
1390 vector<VerificationNote>& notes,
1392 bool* have_main_subtitle,
1393 bool* have_no_main_subtitle,
1394 size_t* most_closed_captions,
1395 size_t* fewest_closed_captions,
1396 map<Marker, Time>* markers_seen
1399 for (auto i: reel->assets()) {
1400 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1401 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1403 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1404 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1406 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1407 if (i->encryptable() && !file_asset->hash()) {
1408 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1412 if (dcp->standard() == Standard::SMPTE) {
1413 boost::optional<int64_t> duration;
1414 for (auto i: reel->assets()) {
1416 duration = i->actual_duration();
1417 } else if (*duration != i->actual_duration()) {
1418 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1424 if (reel->main_picture()) {
1425 /* Check reel stuff */
1426 auto const frame_rate = reel->main_picture()->frame_rate();
1427 if (frame_rate.denominator != 1 ||
1428 (frame_rate.numerator != 24 &&
1429 frame_rate.numerator != 25 &&
1430 frame_rate.numerator != 30 &&
1431 frame_rate.numerator != 48 &&
1432 frame_rate.numerator != 50 &&
1433 frame_rate.numerator != 60 &&
1434 frame_rate.numerator != 96)) {
1436 VerificationNote::Type::ERROR,
1437 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1438 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1442 if (reel->main_picture()->asset_ref().resolved()) {
1443 verify_main_picture_asset(dcp, reel->main_picture(), stage, progress, options, notes);
1444 auto const asset_size = reel->main_picture()->asset()->size();
1445 if (main_picture_active_area) {
1446 if (main_picture_active_area->width > asset_size.width) {
1448 VerificationNote::Type::ERROR,
1449 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1450 String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1454 if (main_picture_active_area->height > asset_size.height) {
1456 VerificationNote::Type::ERROR,
1457 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1458 String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1467 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1468 verify_main_sound_asset(dcp, reel->main_sound(), stage, progress, options, notes, state);
1471 if (reel->main_subtitle()) {
1472 verify_main_subtitle_reel(reel->main_subtitle(), notes);
1473 if (reel->main_subtitle()->asset_ref().resolved()) {
1474 verify_subtitle_asset(reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, xsd_dtd_directory, notes, state);
1476 *have_main_subtitle = true;
1478 *have_no_main_subtitle = true;
1481 for (auto i: reel->closed_captions()) {
1482 verify_closed_caption_reel(i, notes);
1483 if (i->asset_ref().resolved()) {
1484 verify_closed_caption_asset(i->asset(), i->duration(), stage, xsd_dtd_directory, notes);
1488 if (reel->main_markers()) {
1489 for (auto const& i: reel->main_markers()->get()) {
1490 markers_seen->insert(i);
1492 if (reel->main_markers()->entry_point()) {
1493 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_ENTRY_POINT});
1495 if (reel->main_markers()->duration()) {
1496 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_DURATION});
1500 *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1501 *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1509 shared_ptr<const DCP> dcp,
1510 shared_ptr<const CPL> cpl,
1511 function<void (string, optional<boost::filesystem::path>)> stage,
1512 boost::filesystem::path xsd_dtd_directory,
1513 function<void (float)> progress,
1514 VerificationOptions options,
1515 vector<VerificationNote>& notes,
1519 stage("Checking CPL", cpl->file());
1520 validate_xml(cpl->file().get(), xsd_dtd_directory, notes);
1522 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1523 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1526 for (auto const& i: cpl->additional_subtitle_languages()) {
1527 verify_language_tag(i, notes);
1530 if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1531 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1532 * of the approved ones.
1534 auto all = ContentKind::all();
1535 auto name = cpl->content_kind().name();
1536 transform(name.begin(), name.end(), name.begin(), ::tolower);
1537 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1538 if (iter == all.end()) {
1539 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name()});
1543 if (cpl->release_territory()) {
1544 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") {
1545 auto terr = cpl->release_territory().get();
1546 /* Must be a valid region tag, or "001" */
1548 LanguageTag::RegionSubtag test(terr);
1550 if (terr != "001") {
1551 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1557 for (auto version: cpl->content_versions()) {
1558 if (version.label_text.empty()) {
1560 dcp::VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT, cpl->file().get()).set_id(cpl->id())
1566 if (dcp->standard() == Standard::SMPTE) {
1567 if (!cpl->annotation_text()) {
1568 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1569 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1570 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1574 for (auto i: dcp->pkls()) {
1575 /* Check that the CPL's hash corresponds to the PKL */
1576 optional<string> h = i->hash(cpl->id());
1577 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1578 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1581 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1582 optional<string> required_annotation_text;
1583 for (auto j: i->assets()) {
1584 /* See if this is a CPL */
1585 for (auto k: dcp->cpls()) {
1586 if (j->id() == k->id()) {
1587 if (!required_annotation_text) {
1588 /* First CPL we have found; this is the required AnnotationText unless we find another */
1589 required_annotation_text = cpl->content_title_text();
1591 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1592 required_annotation_text = boost::none;
1598 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1599 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1603 /* set to true if any reel has a MainSubtitle */
1604 auto have_main_subtitle = false;
1605 /* set to true if any reel has no MainSubtitle */
1606 auto have_no_main_subtitle = false;
1607 /* fewest number of closed caption assets seen in a reel */
1608 size_t fewest_closed_captions = SIZE_MAX;
1609 /* most number of closed caption assets seen in a reel */
1610 size_t most_closed_captions = 0;
1611 map<Marker, Time> markers_seen;
1613 auto const main_picture_active_area = cpl->main_picture_active_area();
1614 if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1616 VerificationNote::Type::ERROR,
1617 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1618 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1622 if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1624 VerificationNote::Type::ERROR,
1625 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1626 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1631 for (auto reel: cpl->reels()) {
1632 stage("Checking reel", optional<boost::filesystem::path>());
1637 main_picture_active_area,
1644 &have_main_subtitle,
1645 &have_no_main_subtitle,
1646 &most_closed_captions,
1647 &fewest_closed_captions,
1652 verify_text_details(dcp->standard().get_value_or(dcp::Standard::SMPTE), cpl->reels(), notes);
1654 if (dcp->standard() == Standard::SMPTE) {
1655 if (auto msc = cpl->main_sound_configuration()) {
1656 if (state.audio_channels && msc->channels() != *state.audio_channels) {
1658 VerificationNote::Type::ERROR,
1659 VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION,
1660 String::compose("MainSoundConfiguration has %1 channels but sound assets have %2", msc->channels(), *state.audio_channels),
1666 if (have_main_subtitle && have_no_main_subtitle) {
1667 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1670 if (fewest_closed_captions != most_closed_captions) {
1671 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1674 if (cpl->content_kind() == ContentKind::FEATURE) {
1675 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1676 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1678 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1679 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1683 auto ffoc = markers_seen.find(Marker::FFOC);
1684 if (ffoc == markers_seen.end()) {
1685 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1686 } else if (ffoc->second.e != 1) {
1687 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1690 auto lfoc = markers_seen.find(Marker::LFOC);
1691 if (lfoc == markers_seen.end()) {
1692 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1694 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1695 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1696 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1700 LinesCharactersResult result;
1701 for (auto reel: cpl->reels()) {
1702 if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1703 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1707 if (result.line_count_exceeded) {
1708 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1710 if (result.error_length_exceeded) {
1711 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1712 } else if (result.warning_length_exceeded) {
1713 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1716 result = LinesCharactersResult();
1717 for (auto reel: cpl->reels()) {
1718 for (auto i: reel->closed_captions()) {
1720 verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1725 if (result.line_count_exceeded) {
1726 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1728 if (result.error_length_exceeded) {
1729 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1732 if (!cpl->read_composition_metadata()) {
1733 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1734 } else if (!cpl->version_number()) {
1735 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1738 verify_extension_metadata(cpl, notes);
1740 if (cpl->any_encrypted()) {
1741 cxml::Document doc("CompositionPlaylist");
1742 DCP_ASSERT(cpl->file());
1743 doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1744 if (!doc.optional_node_child("Signature")) {
1745 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1755 shared_ptr<const DCP> dcp,
1756 shared_ptr<const PKL> pkl,
1757 boost::filesystem::path xsd_dtd_directory,
1758 vector<VerificationNote>& notes
1761 validate_xml(pkl->file().get(), xsd_dtd_directory, notes);
1763 if (pkl_has_encrypted_assets(dcp, pkl)) {
1764 cxml::Document doc("PackingList");
1765 doc.read_file(dcp::filesystem::fix_long_path(pkl->file().get()));
1766 if (!doc.optional_node_child("Signature")) {
1767 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1771 set<string> uuid_set;
1772 for (auto asset: pkl->assets()) {
1773 if (!uuid_set.insert(asset->id()).second) {
1774 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get()});
1785 shared_ptr<const DCP> dcp,
1786 boost::filesystem::path xsd_dtd_directory,
1787 vector<VerificationNote>& notes
1790 auto asset_map = dcp->asset_map();
1791 DCP_ASSERT(asset_map);
1793 validate_xml(asset_map->file().get(), xsd_dtd_directory, notes);
1795 set<string> uuid_set;
1796 for (auto const& asset: asset_map->assets()) {
1797 if (!uuid_set.insert(asset.id()).second) {
1798 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get()});
1805 vector<VerificationNote>
1807 vector<boost::filesystem::path> directories,
1808 function<void (string, optional<boost::filesystem::path>)> stage,
1809 function<void (float)> progress,
1810 VerificationOptions options,
1811 optional<boost::filesystem::path> xsd_dtd_directory
1814 if (!xsd_dtd_directory) {
1815 xsd_dtd_directory = resources_directory() / "xsd";
1817 *xsd_dtd_directory = filesystem::canonical(*xsd_dtd_directory);
1819 vector<VerificationNote> notes;
1822 vector<shared_ptr<DCP>> dcps;
1823 for (auto i: directories) {
1824 dcps.push_back (make_shared<DCP>(i));
1827 for (auto dcp: dcps) {
1828 stage ("Checking DCP", dcp->directory());
1829 bool carry_on = true;
1831 dcp->read (¬es, true);
1832 } catch (MissingAssetmapError& e) {
1833 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1835 } catch (ReadError& e) {
1836 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1837 } catch (XMLError& e) {
1838 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1839 } catch (MXFFileError& e) {
1840 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1841 } catch (BadURNUUIDError& e) {
1842 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1843 } catch (cxml::Error& e) {
1844 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1851 if (dcp->standard() != Standard::SMPTE) {
1852 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1855 for (auto cpl: dcp->cpls()) {
1868 for (auto pkl: dcp->pkls()) {
1869 stage("Checking PKL", pkl->file());
1870 verify_pkl(dcp, pkl, *xsd_dtd_directory, notes);
1873 if (dcp->asset_map_file()) {
1874 stage("Checking ASSETMAP", dcp->asset_map_file().get());
1875 verify_assetmap(dcp, *xsd_dtd_directory, notes);
1877 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1886 dcp::note_to_string (VerificationNote note)
1888 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1890 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1891 * not "ClosedCaption assets must have an <EntryPoint> tag."
1893 * It's OK to use XML tag names where they are clear.
1894 * If both ID and filename are available, use only the ID.
1895 * End messages with a full stop.
1896 * Messages should not mention whether or not their errors are a part of Bv2.1.
1898 switch (note.code()) {
1899 case VerificationNote::Code::FAILED_READ:
1900 return *note.note();
1901 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1902 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1903 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1904 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1905 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1906 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1907 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1908 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1909 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1910 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1911 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1912 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1913 case VerificationNote::Code::EMPTY_ASSET_PATH:
1914 return "The asset map contains an empty asset path.";
1915 case VerificationNote::Code::MISSING_ASSET:
1916 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1917 case VerificationNote::Code::MISMATCHED_STANDARD:
1918 return "The DCP contains both SMPTE and Interop parts.";
1919 case VerificationNote::Code::INVALID_XML:
1920 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1921 case VerificationNote::Code::MISSING_ASSETMAP:
1922 return "No valid ASSETMAP or ASSETMAP.xml was found.";
1923 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1924 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1925 case VerificationNote::Code::INVALID_DURATION:
1926 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1927 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1928 return String::compose("The instantaneous bit rate of the picture asset %1 is larger than the limit of 250Mbit/s in at least one place.", note.file()->filename());
1929 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1930 return String::compose("The instantaneous bit rate of the picture asset %1 is close to the limit of 250Mbit/s in at least one place.", note.file()->filename());
1931 case VerificationNote::Code::EXTERNAL_ASSET:
1932 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());
1933 case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1934 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1935 case VerificationNote::Code::INVALID_STANDARD:
1936 return "This DCP does not use the SMPTE standard.";
1937 case VerificationNote::Code::INVALID_LANGUAGE:
1938 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1939 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1940 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1941 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1942 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1943 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1944 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1945 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1946 return "3D 4K DCPs are not allowed.";
1947 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1948 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1949 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1950 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1951 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1952 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());
1953 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1954 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1955 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1956 return "Some subtitle assets have different <Language> tags than others";
1957 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1958 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1959 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1960 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1961 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1962 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1963 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1964 return "At least one subtitle lasts less than 15 frames.";
1965 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1966 return "At least one pair of subtitles is separated by less than 2 frames.";
1967 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1968 return "At least one subtitle extends outside of its reel.";
1969 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1970 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1971 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1972 return "There are more than 52 characters in at least one subtitle line.";
1973 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1974 return "There are more than 79 characters in at least one subtitle line.";
1975 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1976 return "There are more than 3 closed caption lines in at least one place.";
1977 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1978 return "There are more than 32 characters in at least one closed caption line.";
1979 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1980 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1981 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1982 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1983 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1984 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.note().get());
1985 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1986 return "All assets in a reel do not have the same duration.";
1987 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
1988 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
1989 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
1990 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1991 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
1992 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
1993 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
1994 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
1995 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
1996 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
1997 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
1998 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
1999 case VerificationNote::Code::MISSING_HASH:
2000 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
2001 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
2002 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
2003 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
2004 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
2005 case VerificationNote::Code::MISSING_FFOC:
2006 return "There should be a FFOC (first frame of content) marker.";
2007 case VerificationNote::Code::MISSING_LFOC:
2008 return "There should be a LFOC (last frame of content) marker.";
2009 case VerificationNote::Code::INCORRECT_FFOC:
2010 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
2011 case VerificationNote::Code::INCORRECT_LFOC:
2012 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
2013 case VerificationNote::Code::MISSING_CPL_METADATA:
2014 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
2015 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
2016 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
2017 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
2018 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
2019 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
2020 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
2021 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
2022 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
2023 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
2024 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
2025 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
2026 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
2027 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
2028 return "Some assets are encrypted but some are not.";
2029 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
2030 return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1).", note.note().get());
2031 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
2032 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
2033 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
2034 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
2035 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
2036 return "The JPEG2000 tile size is not the same as the image size.";
2037 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
2038 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
2039 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
2040 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
2041 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
2042 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
2043 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
2044 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
2045 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
2046 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
2047 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
2048 return "POC marker found outside main header.";
2049 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
2050 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
2051 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
2052 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
2053 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
2054 return "No TLM marker was found in a JPEG2000 codestream.";
2055 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
2056 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
2057 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
2058 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
2059 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
2061 vector<string> parts;
2062 boost::split (parts, note.note().get(), boost::is_any_of(" "));
2063 DCP_ASSERT (parts.size() == 2);
2064 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]);
2066 case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
2067 return "Some aspect of this DCP could not be checked because it is encrypted.";
2068 case VerificationNote::Code::EMPTY_TEXT:
2069 return "There is an empty <Text> node in a subtitle or closed caption.";
2070 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
2071 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
2072 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
2073 return "Some closed captions are not listed in the order of their vertical position.";
2074 case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
2075 return "There is an <EntryPoint> node inside a <MainMarkers>.";
2076 case VerificationNote::Code::UNEXPECTED_DURATION:
2077 return "There is an <Duration> node inside a <MainMarkers>.";
2078 case VerificationNote::Code::INVALID_CONTENT_KIND:
2079 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
2080 case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
2081 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
2082 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
2083 return String::compose("The PKL %1 has more than one asset with the same ID.", note.note().get());
2084 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
2085 return String::compose("The ASSETMAP %1 has more than one asset with the same ID.", note.note().get());
2086 case VerificationNote::Code::MISSING_SUBTITLE:
2087 return String::compose("The subtitle asset %1 has no subtitles.", note.note().get());
2088 case VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
2089 return String::compose("<IssueDate> has an invalid value: %1", note.note().get());
2090 case VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS:
2091 return String::compose("The sound assets do not all have the same channel count; the first to differ is %1", note.file()->filename());
2092 case VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION:
2093 return String::compose("<MainSoundConfiguration> has an invalid value: %1", note.note().get());
2094 case VerificationNote::Code::MISSING_FONT:
2095 return String::compose("The font file for font ID \"%1\" was not found, or was not referred to in the ASSETMAP.", note.note().get());
2096 case VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE:
2097 return String::compose(
2098 "Frame %1 has an image component that is too large (component %2 is %3 bytes in size).",
2099 note.frame().get(), note.component().get(), note.size().get()
2101 case VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT:
2102 return String::compose("The XML in the subtitle asset %1 has more than one namespace declaration.", note.note().get());
2103 case VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT:
2104 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());
2105 case VerificationNote::Code::MISSING_LOAD_FONT:
2106 return String::compose("The SMPTE subtitle asset %1 has <Text> nodes but no <LoadFont> node", note.id().get());
2107 case VerificationNote::Code::MISMATCHED_ASSET_MAP_ID:
2108 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());
2109 case VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT:
2110 return String::compose("The <LabelText> in a <ContentVersion> in CPL %1 is empty", note.id().get());
2118 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2120 return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
2125 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2127 if (a.type() != b.type()) {
2128 return a.type() < b.type();
2131 if (a.code() != b.code()) {
2132 return a.code() < b.code();
2135 if (a.note() != b.note()) {
2136 return a.note().get_value_or("") < b.note().get_value_or("");
2139 if (a.file() != b.file()) {
2140 return a.file().get_value_or("") < b.file().get_value_or("");
2143 return a.line().get_value_or(0) < b.line().get_value_or(0);
2148 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2150 s << note_to_string (note);
2152 s << " [" << note.note().get() << "]";
2155 s << " [" << note.file().get() << "]";
2158 s << " [" << note.line().get() << "]";