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 "interop_subtitle_asset.h"
45 #include "mono_picture_asset.h"
46 #include "mono_picture_frame.h"
47 #include "raw_convert.h"
49 #include "reel_closed_caption_asset.h"
50 #include "reel_interop_subtitle_asset.h"
51 #include "reel_markers_asset.h"
52 #include "reel_picture_asset.h"
53 #include "reel_sound_asset.h"
54 #include "reel_smpte_subtitle_asset.h"
55 #include "reel_subtitle_asset.h"
56 #include "smpte_subtitle_asset.h"
57 #include "stereo_picture_asset.h"
58 #include "stereo_picture_frame.h"
60 #include "verify_j2k.h"
61 #include <libxml/parserInternals.h>
62 #include <xercesc/dom/DOMAttr.hpp>
63 #include <xercesc/dom/DOMDocument.hpp>
64 #include <xercesc/dom/DOMError.hpp>
65 #include <xercesc/dom/DOMErrorHandler.hpp>
66 #include <xercesc/dom/DOMException.hpp>
67 #include <xercesc/dom/DOMImplementation.hpp>
68 #include <xercesc/dom/DOMImplementationLS.hpp>
69 #include <xercesc/dom/DOMImplementationRegistry.hpp>
70 #include <xercesc/dom/DOMLSParser.hpp>
71 #include <xercesc/dom/DOMLocator.hpp>
72 #include <xercesc/dom/DOMNamedNodeMap.hpp>
73 #include <xercesc/dom/DOMNodeList.hpp>
74 #include <xercesc/framework/LocalFileInputSource.hpp>
75 #include <xercesc/framework/MemBufInputSource.hpp>
76 #include <xercesc/parsers/AbstractDOMParser.hpp>
77 #include <xercesc/parsers/XercesDOMParser.hpp>
78 #include <xercesc/sax/HandlerBase.hpp>
79 #include <xercesc/util/PlatformUtils.hpp>
80 #include <boost/algorithm/string.hpp>
89 using std::dynamic_pointer_cast;
91 using std::make_shared;
95 using std::shared_ptr;
98 using boost::optional;
99 using boost::function;
103 using namespace xercesc;
108 xml_ch_to_string (XMLCh const * a)
110 char* x = XMLString::transcode(a);
112 XMLString::release(&x);
117 class XMLValidationError
120 XMLValidationError (SAXParseException const & e)
121 : _message (xml_ch_to_string(e.getMessage()))
122 , _line (e.getLineNumber())
123 , _column (e.getColumnNumber())
124 , _public_id (e.getPublicId() ? xml_ch_to_string(e.getPublicId()) : "")
125 , _system_id (e.getSystemId() ? xml_ch_to_string(e.getSystemId()) : "")
130 string message () const {
134 uint64_t line () const {
138 uint64_t column () const {
142 string public_id () const {
146 string system_id () const {
159 class DCPErrorHandler : public ErrorHandler
162 void warning(const SAXParseException& e) override
164 maybe_add (XMLValidationError(e));
167 void error(const SAXParseException& e) override
169 maybe_add (XMLValidationError(e));
172 void fatalError(const SAXParseException& e) override
174 maybe_add (XMLValidationError(e));
177 void resetErrors() override {
181 list<XMLValidationError> errors () const {
186 void maybe_add (XMLValidationError e)
188 /* XXX: nasty hack */
190 e.message().find("schema document") != string::npos &&
191 e.message().find("has different target namespace from the one specified in instance document") != string::npos
196 _errors.push_back (e);
199 list<XMLValidationError> _errors;
206 StringToXMLCh (string a)
208 _buffer = XMLString::transcode(a.c_str());
211 StringToXMLCh (StringToXMLCh const&) = delete;
212 StringToXMLCh& operator= (StringToXMLCh const&) = delete;
216 XMLString::release (&_buffer);
219 XMLCh const * get () const {
228 class LocalFileResolver : public EntityResolver
231 LocalFileResolver (boost::filesystem::path xsd_dtd_directory)
232 : _xsd_dtd_directory (xsd_dtd_directory)
234 /* XXX: I'm not clear on what things need to be in this list; some XSDs are apparently, magically
235 * found without being here.
237 add("http://www.w3.org/2001/XMLSchema.dtd", "XMLSchema.dtd");
238 add("http://www.w3.org/2001/03/xml.xsd", "xml.xsd");
239 add("http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd", "xmldsig-core-schema.xsd");
240 add("http://www.digicine.com/schemas/437-Y/2007/Main-Stereo-Picture-CPL.xsd", "Main-Stereo-Picture-CPL.xsd");
241 add("http://www.digicine.com/PROTO-ASDCP-CPL-20040511.xsd", "PROTO-ASDCP-CPL-20040511.xsd");
242 add("http://www.digicine.com/PROTO-ASDCP-PKL-20040311.xsd", "PROTO-ASDCP-PKL-20040311.xsd");
243 add("http://www.digicine.com/PROTO-ASDCP-AM-20040311.xsd", "PROTO-ASDCP-AM-20040311.xsd");
244 add("http://www.digicine.com/PROTO-ASDCP-CC-CPL-20070926#", "PROTO-ASDCP-CC-CPL-20070926.xsd");
245 add("interop-subs", "DCSubtitle.v1.mattsson.xsd");
246 add("http://www.smpte-ra.org/schemas/428-7/2010/DCST.xsd", "DCDMSubtitle-2010.xsd");
247 add("http://www.smpte-ra.org/schemas/428-7/2014/DCST.xsd", "DCDMSubtitle-2014.xsd");
248 add("http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata", "SMPTE-429-16.xsd");
249 add("http://www.dolby.com/schemas/2012/AD", "Dolby-2012-AD.xsd");
250 add("http://www.smpte-ra.org/schemas/429-10/2008/Main-Stereo-Picture-CPL", "SMPTE-429-10-2008.xsd");
253 InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id) override
258 auto system_id_str = xml_ch_to_string (system_id);
259 auto p = _xsd_dtd_directory;
260 if (_files.find(system_id_str) == _files.end()) {
263 p /= _files[system_id_str];
265 StringToXMLCh ch (p.string());
266 return new LocalFileInputSource(ch.get());
270 void add (string uri, string file)
275 std::map<string, string> _files;
276 boost::filesystem::path _xsd_dtd_directory;
281 parse (XercesDOMParser& parser, boost::filesystem::path xml)
283 parser.parse(xml.c_str());
288 parse (XercesDOMParser& parser, string xml)
290 xercesc::MemBufInputSource buf(reinterpret_cast<unsigned char const*>(xml.c_str()), xml.size(), "");
297 validate_xml (T xml, boost::filesystem::path xsd_dtd_directory, vector<VerificationNote>& notes)
300 XMLPlatformUtils::Initialize ();
301 } catch (XMLException& e) {
302 throw MiscError ("Failed to initialise xerces library");
305 DCPErrorHandler error_handler;
307 /* All the xerces objects in this scope must be destroyed before XMLPlatformUtils::Terminate() is called */
309 XercesDOMParser parser;
310 parser.setValidationScheme(XercesDOMParser::Val_Always);
311 parser.setDoNamespaces(true);
312 parser.setDoSchema(true);
314 vector<string> schema;
315 schema.push_back("xml.xsd");
316 schema.push_back("xmldsig-core-schema.xsd");
317 schema.push_back("SMPTE-429-7-2006-CPL.xsd");
318 schema.push_back("SMPTE-429-8-2006-PKL.xsd");
319 schema.push_back("SMPTE-429-9-2007-AM.xsd");
320 schema.push_back("Main-Stereo-Picture-CPL.xsd");
321 schema.push_back("PROTO-ASDCP-CPL-20040511.xsd");
322 schema.push_back("PROTO-ASDCP-PKL-20040311.xsd");
323 schema.push_back("PROTO-ASDCP-AM-20040311.xsd");
324 schema.push_back("DCSubtitle.v1.mattsson.xsd");
325 schema.push_back("DCDMSubtitle-2010.xsd");
326 schema.push_back("DCDMSubtitle-2014.xsd");
327 schema.push_back("PROTO-ASDCP-CC-CPL-20070926.xsd");
328 schema.push_back("SMPTE-429-16.xsd");
329 schema.push_back("Dolby-2012-AD.xsd");
330 schema.push_back("SMPTE-429-10-2008.xsd");
331 schema.push_back("xlink.xsd");
332 schema.push_back("SMPTE-335-2012.xsd");
333 schema.push_back("SMPTE-395-2014-13-1-aaf.xsd");
334 schema.push_back("isdcf-mca.xsd");
335 schema.push_back("SMPTE-429-12-2008.xsd");
337 /* XXX: I'm not especially clear what this is for, but it seems to be necessary.
338 * Schemas that are not mentioned in this list are not read, and the things
339 * they describe are not checked.
342 for (auto i: schema) {
343 locations += String::compose("%1 %1 ", i, i);
346 parser.setExternalSchemaLocation(locations.c_str());
347 parser.setValidationSchemaFullChecking(true);
348 parser.setErrorHandler(&error_handler);
350 LocalFileResolver resolver (xsd_dtd_directory);
351 parser.setEntityResolver(&resolver);
354 parser.resetDocumentPool();
356 } catch (XMLException& e) {
357 throw MiscError(xml_ch_to_string(e.getMessage()));
358 } catch (DOMException& e) {
359 throw MiscError(xml_ch_to_string(e.getMessage()));
361 throw MiscError("Unknown exception from xerces");
365 XMLPlatformUtils::Terminate ();
367 for (auto i: error_handler.errors()) {
369 VerificationNote::Type::ERROR,
370 VerificationNote::Code::INVALID_XML,
372 boost::trim_copy(i.public_id() + " " + i.system_id()),
379 enum class VerifyAssetResult {
386 static VerifyAssetResult
387 verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelFileAsset> reel_file_asset, function<void (float)> progress)
389 /* When reading the DCP the hash will have been set to the one from the PKL/CPL.
390 * We want to calculate the hash of the actual file contents here, so that we
391 * can check it. unset_hash() means that this calculation will happen on the
394 reel_file_asset->asset_ref()->unset_hash();
395 auto const actual_hash = reel_file_asset->asset_ref()->hash(progress);
397 auto pkls = dcp->pkls();
398 /* We've read this DCP in so it must have at least one PKL */
399 DCP_ASSERT (!pkls.empty());
401 auto asset = reel_file_asset->asset_ref().asset();
403 optional<string> pkl_hash;
405 pkl_hash = i->hash (reel_file_asset->asset_ref()->id());
411 DCP_ASSERT (pkl_hash);
413 auto cpl_hash = reel_file_asset->hash();
414 if (cpl_hash && *cpl_hash != *pkl_hash) {
415 return VerifyAssetResult::CPL_PKL_DIFFER;
418 if (actual_hash != *pkl_hash) {
419 return VerifyAssetResult::BAD;
422 return VerifyAssetResult::GOOD;
427 verify_language_tag (string tag, vector<VerificationNote>& notes)
430 LanguageTag test (tag);
431 } catch (LanguageTagError &) {
432 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, tag});
438 verify_picture_asset (shared_ptr<const ReelFileAsset> reel_file_asset, boost::filesystem::path file, vector<VerificationNote>& notes, function<void (float)> progress)
440 int biggest_frame = 0;
441 auto asset = dynamic_pointer_cast<PictureAsset>(reel_file_asset->asset_ref().asset());
442 auto const duration = asset->intrinsic_duration ();
444 auto check_and_add = [¬es](vector<VerificationNote> const& j2k_notes) {
445 for (auto i: j2k_notes) {
446 if (find(notes.begin(), notes.end(), i) == notes.end()) {
452 if (auto mono_asset = dynamic_pointer_cast<MonoPictureAsset>(reel_file_asset->asset_ref().asset())) {
453 auto reader = mono_asset->start_read ();
454 for (int64_t i = 0; i < duration; ++i) {
455 auto frame = reader->get_frame (i);
456 biggest_frame = max(biggest_frame, frame->size());
457 if (!mono_asset->encrypted() || mono_asset->key()) {
458 vector<VerificationNote> j2k_notes;
459 verify_j2k(frame, i, mono_asset->frame_rate().numerator, j2k_notes);
460 check_and_add (j2k_notes);
462 progress (float(i) / duration);
464 } else if (auto stereo_asset = dynamic_pointer_cast<StereoPictureAsset>(asset)) {
465 auto reader = stereo_asset->start_read ();
466 for (int64_t i = 0; i < duration; ++i) {
467 auto frame = reader->get_frame (i);
468 biggest_frame = max(biggest_frame, max(frame->left()->size(), frame->right()->size()));
469 if (!stereo_asset->encrypted() || stereo_asset->key()) {
470 vector<VerificationNote> j2k_notes;
471 verify_j2k(frame->left(), i, stereo_asset->frame_rate().numerator, j2k_notes);
472 verify_j2k(frame->right(), i, stereo_asset->frame_rate().numerator, j2k_notes);
473 check_and_add (j2k_notes);
475 progress (float(i) / duration);
480 static const int max_frame = rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
481 static const int risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
482 if (biggest_frame > max_frame) {
484 VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
486 } else if (biggest_frame > risky_frame) {
488 VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
495 verify_main_picture_asset (
496 shared_ptr<const DCP> dcp,
497 shared_ptr<const ReelPictureAsset> reel_asset,
498 function<void (string, optional<boost::filesystem::path>)> stage,
499 function<void (float)> progress,
500 VerificationOptions options,
501 vector<VerificationNote>& notes
504 auto asset = reel_asset->asset();
505 auto const file = *asset->file();
507 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || boost::filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
508 stage ("Checking picture asset hash", file);
509 auto const r = verify_asset (dcp, reel_asset, progress);
511 case VerifyAssetResult::BAD:
513 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file
516 case VerifyAssetResult::CPL_PKL_DIFFER:
518 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file
526 stage ("Checking picture frame sizes", asset->file());
527 verify_picture_asset (reel_asset, file, notes, progress);
529 /* Only flat/scope allowed by Bv2.1 */
531 asset->size() != Size(2048, 858) &&
532 asset->size() != Size(1998, 1080) &&
533 asset->size() != Size(4096, 1716) &&
534 asset->size() != Size(3996, 2160)) {
536 VerificationNote::Type::BV21_ERROR,
537 VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS,
538 String::compose("%1x%2", asset->size().width, asset->size().height),
543 /* Only 24, 25, 48fps allowed for 2K */
545 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
546 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
549 VerificationNote::Type::BV21_ERROR,
550 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
551 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
556 if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
557 /* Only 24fps allowed for 4K */
558 if (asset->edit_rate() != Fraction(24, 1)) {
560 VerificationNote::Type::BV21_ERROR,
561 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
562 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
567 /* Only 2D allowed for 4K */
568 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
570 VerificationNote::Type::BV21_ERROR,
571 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
572 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
584 boost::optional<string> subtitle_language;
585 boost::optional<int> audio_channels;
590 verify_main_sound_asset (
591 shared_ptr<const DCP> dcp,
592 shared_ptr<const ReelSoundAsset> reel_asset,
593 function<void (string, optional<boost::filesystem::path>)> stage,
594 function<void (float)> progress,
595 VerificationOptions options,
596 vector<VerificationNote>& notes,
600 auto asset = reel_asset->asset();
601 auto const file = *asset->file();
603 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || boost::filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
604 stage("Checking sound asset hash", file);
605 auto const r = verify_asset (dcp, reel_asset, progress);
607 case VerifyAssetResult::BAD:
608 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, file});
610 case VerifyAssetResult::CPL_PKL_DIFFER:
611 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, file});
618 if (!state.audio_channels) {
619 state.audio_channels = asset->channels();
620 } else if (*state.audio_channels != asset->channels()) {
621 notes.push_back({ VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS, file });
624 stage ("Checking sound asset metadata", file);
626 if (auto lang = asset->language()) {
627 verify_language_tag (*lang, notes);
629 if (asset->sampling_rate() != 48000) {
630 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), file});
636 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
638 /* XXX: is Language compulsory? */
639 if (reel_asset->language()) {
640 verify_language_tag (*reel_asset->language(), notes);
643 if (!reel_asset->entry_point()) {
644 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id() });
645 } else if (reel_asset->entry_point().get()) {
646 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
652 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
654 /* XXX: is Language compulsory? */
655 if (reel_asset->language()) {
656 verify_language_tag (*reel_asset->language(), notes);
659 if (!reel_asset->entry_point()) {
660 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
661 } else if (reel_asset->entry_point().get()) {
662 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
667 /** Verify stuff that is common to both subtitles and closed captions */
669 verify_smpte_timed_text_asset (
670 shared_ptr<const SMPTESubtitleAsset> asset,
671 optional<int64_t> reel_asset_duration,
672 vector<VerificationNote>& notes
675 if (asset->language()) {
676 verify_language_tag (*asset->language(), notes);
678 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
681 auto const size = boost::filesystem::file_size(asset->file().get());
682 if (size > 115 * 1024 * 1024) {
684 { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file() }
688 /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
689 * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
691 auto fonts = asset->font_data ();
693 for (auto i: fonts) {
694 total_size += i.second.size();
696 if (total_size > 10 * 1024 * 1024) {
697 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
700 if (!asset->start_time()) {
701 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get() });
702 } else if (asset->start_time() != Time()) {
703 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() });
706 if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
709 VerificationNote::Type::BV21_ERROR,
710 VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
711 String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
718 /** Verify Interop subtitle / CCAP stuff */
720 verify_interop_text_asset(shared_ptr<const InteropSubtitleAsset> asset, vector<VerificationNote>& notes)
722 if (asset->subtitles().empty()) {
723 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_SUBTITLE, asset->id(), asset->file().get() });
725 auto const unresolved = asset->unresolved_fonts();
726 if (!unresolved.empty()) {
727 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_FONT, unresolved.front() });
732 /** Verify SMPTE subtitle-only stuff */
734 verify_smpte_subtitle_asset (
735 shared_ptr<const SMPTESubtitleAsset> asset,
736 vector<VerificationNote>& notes,
740 if (asset->language()) {
741 if (!state.subtitle_language) {
742 state.subtitle_language = *asset->language();
743 } else if (state.subtitle_language != *asset->language()) {
744 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
748 DCP_ASSERT (asset->resource_id());
749 auto xml_id = asset->xml_id();
751 if (asset->resource_id().get() != xml_id) {
752 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
755 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
756 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
759 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
762 if (asset->raw_xml()) {
763 /* Deluxe require this in their QC even if it seems never to be mentioned in any standard */
764 cxml::Document doc("SubtitleReel");
765 doc.read_string(*asset->raw_xml());
766 auto issue_date = doc.string_child("IssueDate");
767 std::regex reg("^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d$");
768 if (!std::regex_match(issue_date, reg)) {
769 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, issue_date});
775 /** Verify all subtitle stuff */
777 verify_subtitle_asset (
778 shared_ptr<const SubtitleAsset> asset,
779 optional<int64_t> reel_asset_duration,
780 function<void (string, optional<boost::filesystem::path>)> stage,
781 boost::filesystem::path xsd_dtd_directory,
782 vector<VerificationNote>& notes,
786 stage ("Checking subtitle XML", asset->file());
787 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
788 * gets passed through libdcp which may clean up and therefore hide errors.
790 if (asset->raw_xml()) {
791 validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
793 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
796 auto namespace_count = [](shared_ptr<const SubtitleAsset> asset, string root_node) {
797 cxml::Document doc(root_node);
798 doc.read_string(asset->raw_xml().get());
799 auto root = dynamic_cast<xmlpp::Element*>(doc.node())->cobj();
801 for (auto ns = root->nsDef; ns != nullptr; ns = ns->next) {
807 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
809 verify_interop_text_asset(interop, notes);
810 if (namespace_count(asset, "DCSubtitle") > 1) {
811 notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id() });
815 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
817 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
818 verify_smpte_subtitle_asset (smpte, notes, state);
819 /* This asset may be encrypted and in that case we'll have no raw_xml() */
820 if (asset->raw_xml() && namespace_count(asset, "SubtitleReel") > 1) {
821 notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id()});
827 /** Verify all closed caption stuff */
829 verify_closed_caption_asset (
830 shared_ptr<const SubtitleAsset> asset,
831 optional<int64_t> reel_asset_duration,
832 function<void (string, optional<boost::filesystem::path>)> stage,
833 boost::filesystem::path xsd_dtd_directory,
834 vector<VerificationNote>& notes
837 stage ("Checking closed caption XML", asset->file());
838 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
839 * gets passed through libdcp which may clean up and therefore hide errors.
841 auto raw_xml = asset->raw_xml();
843 validate_xml (*raw_xml, xsd_dtd_directory, notes);
844 if (raw_xml->size() > 256 * 1024) {
845 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
848 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
851 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
853 verify_interop_text_asset(interop, notes);
856 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
858 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
863 /** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes etc. */
866 verify_text_details (
867 dcp::Standard standard,
868 vector<shared_ptr<Reel>> reels,
870 vector<VerificationNote>& notes,
871 std::function<bool (shared_ptr<Reel>)> check,
872 std::function<optional<string> (shared_ptr<Reel>)> xml,
873 std::function<int64_t (shared_ptr<Reel>)> duration,
874 std::function<std::string (shared_ptr<Reel>)> id
877 /* end of last subtitle (in editable units) */
878 optional<int64_t> last_out;
879 auto too_short = false;
880 auto too_close = false;
881 auto too_early = false;
882 auto reel_overlap = false;
883 auto empty_text = false;
884 /* current reel start time (in editable units) */
885 int64_t reel_offset = 0;
886 optional<string> missing_load_font_id;
888 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool, bool&, vector<string>&)> parse;
890 parse = [&parse, &last_out, &too_short, &too_close, &too_early, &empty_text, &reel_offset, &missing_load_font_id](
891 cxml::ConstNodePtr node,
893 optional<Time> start_time,
897 vector<string>& font_ids
899 if (node->name() == "Subtitle") {
900 Time in (node->string_attribute("TimeIn"), tcr);
904 Time out (node->string_attribute("TimeOut"), tcr);
908 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
911 auto length = out - in;
912 if (length.as_editable_units_ceil(er) < 15) {
916 /* XXX: this feels dubious - is it really what Bv2.1 means? */
917 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
918 if (distance >= 0 && distance < 2) {
922 last_out = reel_offset + out.as_editable_units_floor(er);
923 } else if (node->name() == "Text") {
924 std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
925 if (!node->content().empty()) {
928 for (auto i: node->node_children()) {
929 if (node_has_content(i)) {
935 if (!node_has_content(node)) {
939 } else if (node->name() == "LoadFont") {
940 if (auto const id = node->optional_string_attribute("Id")) {
941 font_ids.push_back(*id);
942 } else if (auto const id = node->optional_string_attribute("ID")) {
943 font_ids.push_back(*id);
945 } else if (node->name() == "Font") {
946 if (auto const font_id = node->optional_string_attribute("Id")) {
947 if (std::find_if(font_ids.begin(), font_ids.end(), [font_id](string const& id) { return id == font_id; }) == font_ids.end()) {
948 missing_load_font_id = font_id;
952 for (auto i: node->node_children()) {
953 parse(i, tcr, start_time, er, first_reel, has_text, font_ids);
957 for (auto i = 0U; i < reels.size(); ++i) {
958 if (!check(reels[i])) {
962 auto reel_xml = xml(reels[i]);
964 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
968 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
969 * read in by libdcp's parser.
972 shared_ptr<cxml::Document> doc;
974 optional<Time> start_time;
976 case dcp::Standard::INTEROP:
977 doc = make_shared<cxml::Document>("DCSubtitle");
978 doc->read_string (*reel_xml);
980 case dcp::Standard::SMPTE:
981 doc = make_shared<cxml::Document>("SubtitleReel");
982 doc->read_string (*reel_xml);
983 tcr = doc->number_child<int>("TimeCodeRate");
984 if (auto start_time_string = doc->optional_string_child("StartTime")) {
985 start_time = Time(*start_time_string, tcr);
989 bool has_text = false;
990 vector<string> font_ids;
991 parse(doc, tcr, start_time, edit_rate, i == 0, has_text, font_ids);
992 auto end = reel_offset + duration(reels[i]);
993 if (last_out && *last_out > end) {
998 if (standard == dcp::Standard::SMPTE && has_text && font_ids.empty()) {
999 notes.push_back(dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT).set_id(id(reels[i])));
1003 if (last_out && *last_out > reel_offset) {
1004 reel_overlap = true;
1009 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
1015 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
1021 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
1027 VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
1033 VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_TEXT
1037 if (missing_load_font_id) {
1038 notes.push_back(dcp::VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT).set_id(*missing_load_font_id));
1045 verify_closed_caption_details (
1046 vector<shared_ptr<Reel>> reels,
1047 vector<VerificationNote>& notes
1050 std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
1051 find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
1052 for (auto i: node->node_children()) {
1053 if (i->name() == "Text") {
1054 text_or_image.push_back (i);
1056 find_text_or_image (i, text_or_image);
1061 auto mismatched_valign = false;
1062 auto incorrect_order = false;
1064 std::function<void (cxml::ConstNodePtr)> parse;
1065 parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
1066 if (node->name() == "Subtitle") {
1067 vector<cxml::ConstNodePtr> text_or_image;
1068 find_text_or_image (node, text_or_image);
1069 optional<string> last_valign;
1070 optional<float> last_vpos;
1071 for (auto i: text_or_image) {
1072 auto valign = i->optional_string_attribute("VAlign");
1074 valign = i->optional_string_attribute("Valign").get_value_or("center");
1076 auto vpos = i->optional_number_attribute<float>("VPosition");
1078 vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
1082 if (*last_valign != valign) {
1083 mismatched_valign = true;
1086 last_valign = valign;
1088 if (!mismatched_valign) {
1090 if (*last_valign == "top" || *last_valign == "center") {
1091 if (*vpos < *last_vpos) {
1092 incorrect_order = true;
1095 if (*vpos > *last_vpos) {
1096 incorrect_order = true;
1105 for (auto i: node->node_children()) {
1110 for (auto reel: reels) {
1111 for (auto ccap: reel->closed_captions()) {
1112 auto reel_xml = ccap->asset()->raw_xml();
1114 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
1118 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1119 * read in by libdcp's parser.
1122 shared_ptr<cxml::Document> doc;
1124 optional<Time> start_time;
1126 doc = make_shared<cxml::Document>("SubtitleReel");
1127 doc->read_string (*reel_xml);
1129 doc = make_shared<cxml::Document>("DCSubtitle");
1130 doc->read_string (*reel_xml);
1136 if (mismatched_valign) {
1138 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN,
1142 if (incorrect_order) {
1144 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING,
1150 struct LinesCharactersResult
1152 bool warning_length_exceeded = false;
1153 bool error_length_exceeded = false;
1154 bool line_count_exceeded = false;
1160 verify_text_lines_and_characters (
1161 shared_ptr<SubtitleAsset> asset,
1164 LinesCharactersResult* result
1170 Event (Time time_, float position_, int characters_)
1172 , position (position_)
1173 , characters (characters_)
1176 Event (Time time_, shared_ptr<Event> start_)
1182 int position; //< position from 0 at top of screen to 100 at bottom
1184 shared_ptr<Event> start;
1187 vector<shared_ptr<Event>> events;
1189 auto position = [](shared_ptr<const SubtitleString> sub) {
1190 switch (sub->v_align()) {
1192 return lrintf(sub->v_position() * 100);
1193 case VAlign::CENTER:
1194 return lrintf((0.5f + sub->v_position()) * 100);
1195 case VAlign::BOTTOM:
1196 return lrintf((1.0f - sub->v_position()) * 100);
1202 for (auto j: asset->subtitles()) {
1203 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1205 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1206 events.push_back(in);
1207 events.push_back(make_shared<Event>(text->out(), in));
1211 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1212 return a->time < b->time;
1215 map<int, int> current;
1216 for (auto i: events) {
1217 if (current.size() > 3) {
1218 result->line_count_exceeded = true;
1220 for (auto j: current) {
1221 if (j.second > warning_length) {
1222 result->warning_length_exceeded = true;
1224 if (j.second > error_length) {
1225 result->error_length_exceeded = true;
1230 /* end of a subtitle */
1231 DCP_ASSERT (current.find(i->start->position) != current.end());
1232 if (current[i->start->position] == i->start->characters) {
1233 current.erase(i->start->position);
1235 current[i->start->position] -= i->start->characters;
1238 /* start of a subtitle */
1239 if (current.find(i->position) == current.end()) {
1240 current[i->position] = i->characters;
1242 current[i->position] += i->characters;
1251 verify_text_details(dcp::Standard standard, vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1253 if (reels.empty()) {
1257 if (reels[0]->main_subtitle()) {
1258 verify_text_details(standard, reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1259 [](shared_ptr<Reel> reel) {
1260 return static_cast<bool>(reel->main_subtitle());
1262 [](shared_ptr<Reel> reel) {
1263 return reel->main_subtitle()->asset()->raw_xml();
1265 [](shared_ptr<Reel> reel) {
1266 return reel->main_subtitle()->actual_duration();
1268 [](shared_ptr<Reel> reel) {
1269 return reel->main_subtitle()->id();
1274 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1275 verify_text_details(standard, reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1276 [i](shared_ptr<Reel> reel) {
1277 return i < reel->closed_captions().size();
1279 [i](shared_ptr<Reel> reel) {
1280 return reel->closed_captions()[i]->asset()->raw_xml();
1282 [i](shared_ptr<Reel> reel) {
1283 return reel->closed_captions()[i]->actual_duration();
1285 [i](shared_ptr<Reel> reel) {
1286 return reel->closed_captions()[i]->id();
1291 verify_closed_caption_details (reels, notes);
1296 verify_extension_metadata(shared_ptr<const CPL> cpl, vector<VerificationNote>& notes)
1298 DCP_ASSERT (cpl->file());
1299 cxml::Document doc ("CompositionPlaylist");
1300 doc.read_file (cpl->file().get());
1302 auto missing = false;
1305 if (auto reel_list = doc.node_child("ReelList")) {
1306 auto reels = reel_list->node_children("Reel");
1307 if (!reels.empty()) {
1308 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1309 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1310 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1312 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1313 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1317 if (auto name = extension->optional_node_child("Name")) {
1318 if (name->content() != "Application") {
1319 malformed = "<Name> should be 'Application'";
1322 if (auto property_list = extension->optional_node_child("PropertyList")) {
1323 if (auto property = property_list->optional_node_child("Property")) {
1324 if (auto name = property->optional_node_child("Name")) {
1325 if (name->content() != "DCP Constraints Profile") {
1326 malformed = "<Name> property should be 'DCP Constraints Profile'";
1329 if (auto value = property->optional_node_child("Value")) {
1330 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1331 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1346 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1347 } else if (!malformed.empty()) {
1348 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1354 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1356 vector<string> encrypted;
1357 for (auto i: dcp->cpls()) {
1358 for (auto j: i->reel_file_assets()) {
1359 if (j->asset_ref().resolved()) {
1360 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1361 if (mxf && mxf->encrypted()) {
1362 encrypted.push_back(j->asset_ref().id());
1368 for (auto i: pkl->assets()) {
1369 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1381 shared_ptr<const DCP> dcp,
1382 shared_ptr<const CPL> cpl,
1383 shared_ptr<const Reel> reel,
1384 optional<dcp::Size> main_picture_active_area,
1385 function<void (string, optional<boost::filesystem::path>)> stage,
1386 boost::filesystem::path xsd_dtd_directory,
1387 function<void (float)> progress,
1388 VerificationOptions options,
1389 vector<VerificationNote>& notes,
1391 bool* have_main_subtitle,
1392 bool* have_no_main_subtitle,
1393 size_t* most_closed_captions,
1394 size_t* fewest_closed_captions,
1395 map<Marker, Time>* markers_seen
1398 for (auto i: reel->assets()) {
1399 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1400 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1402 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1403 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1405 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1406 if (i->encryptable() && !file_asset->hash()) {
1407 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1411 if (dcp->standard() == Standard::SMPTE) {
1412 boost::optional<int64_t> duration;
1413 for (auto i: reel->assets()) {
1415 duration = i->actual_duration();
1416 } else if (*duration != i->actual_duration()) {
1417 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1423 if (reel->main_picture()) {
1424 /* Check reel stuff */
1425 auto const frame_rate = reel->main_picture()->frame_rate();
1426 if (frame_rate.denominator != 1 ||
1427 (frame_rate.numerator != 24 &&
1428 frame_rate.numerator != 25 &&
1429 frame_rate.numerator != 30 &&
1430 frame_rate.numerator != 48 &&
1431 frame_rate.numerator != 50 &&
1432 frame_rate.numerator != 60 &&
1433 frame_rate.numerator != 96)) {
1435 VerificationNote::Type::ERROR,
1436 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1437 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1441 if (reel->main_picture()->asset_ref().resolved()) {
1442 verify_main_picture_asset(dcp, reel->main_picture(), stage, progress, options, notes);
1443 auto const asset_size = reel->main_picture()->asset()->size();
1444 if (main_picture_active_area) {
1445 if (main_picture_active_area->width > asset_size.width) {
1447 VerificationNote::Type::ERROR,
1448 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1449 String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1453 if (main_picture_active_area->height > asset_size.height) {
1455 VerificationNote::Type::ERROR,
1456 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1457 String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1466 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1467 verify_main_sound_asset(dcp, reel->main_sound(), stage, progress, options, notes, state);
1470 if (reel->main_subtitle()) {
1471 verify_main_subtitle_reel(reel->main_subtitle(), notes);
1472 if (reel->main_subtitle()->asset_ref().resolved()) {
1473 verify_subtitle_asset(reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, xsd_dtd_directory, notes, state);
1475 *have_main_subtitle = true;
1477 *have_no_main_subtitle = true;
1480 for (auto i: reel->closed_captions()) {
1481 verify_closed_caption_reel(i, notes);
1482 if (i->asset_ref().resolved()) {
1483 verify_closed_caption_asset(i->asset(), i->duration(), stage, xsd_dtd_directory, notes);
1487 if (reel->main_markers()) {
1488 for (auto const& i: reel->main_markers()->get()) {
1489 markers_seen->insert(i);
1491 if (reel->main_markers()->entry_point()) {
1492 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_ENTRY_POINT});
1494 if (reel->main_markers()->duration()) {
1495 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_DURATION});
1499 *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1500 *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1508 shared_ptr<const DCP> dcp,
1509 shared_ptr<const CPL> cpl,
1510 function<void (string, optional<boost::filesystem::path>)> stage,
1511 boost::filesystem::path xsd_dtd_directory,
1512 function<void (float)> progress,
1513 VerificationOptions options,
1514 vector<VerificationNote>& notes,
1518 stage("Checking CPL", cpl->file());
1519 validate_xml(cpl->file().get(), xsd_dtd_directory, notes);
1521 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1522 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1525 for (auto const& i: cpl->additional_subtitle_languages()) {
1526 verify_language_tag(i, notes);
1529 if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1530 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1531 * of the approved ones.
1533 auto all = ContentKind::all();
1534 auto name = cpl->content_kind().name();
1535 transform(name.begin(), name.end(), name.begin(), ::tolower);
1536 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1537 if (iter == all.end()) {
1538 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name()});
1542 if (cpl->release_territory()) {
1543 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") {
1544 auto terr = cpl->release_territory().get();
1545 /* Must be a valid region tag, or "001" */
1547 LanguageTag::RegionSubtag test(terr);
1549 if (terr != "001") {
1550 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1556 for (auto version: cpl->content_versions()) {
1557 if (version.label_text.empty()) {
1559 dcp::VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT, cpl->file().get()).set_id(cpl->id())
1565 if (dcp->standard() == Standard::SMPTE) {
1566 if (!cpl->annotation_text()) {
1567 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1568 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1569 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1573 for (auto i: dcp->pkls()) {
1574 /* Check that the CPL's hash corresponds to the PKL */
1575 optional<string> h = i->hash(cpl->id());
1576 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1577 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1580 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1581 optional<string> required_annotation_text;
1582 for (auto j: i->assets()) {
1583 /* See if this is a CPL */
1584 for (auto k: dcp->cpls()) {
1585 if (j->id() == k->id()) {
1586 if (!required_annotation_text) {
1587 /* First CPL we have found; this is the required AnnotationText unless we find another */
1588 required_annotation_text = cpl->content_title_text();
1590 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1591 required_annotation_text = boost::none;
1597 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1598 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1602 /* set to true if any reel has a MainSubtitle */
1603 auto have_main_subtitle = false;
1604 /* set to true if any reel has no MainSubtitle */
1605 auto have_no_main_subtitle = false;
1606 /* fewest number of closed caption assets seen in a reel */
1607 size_t fewest_closed_captions = SIZE_MAX;
1608 /* most number of closed caption assets seen in a reel */
1609 size_t most_closed_captions = 0;
1610 map<Marker, Time> markers_seen;
1612 auto const main_picture_active_area = cpl->main_picture_active_area();
1613 if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1615 VerificationNote::Type::ERROR,
1616 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1617 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1621 if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1623 VerificationNote::Type::ERROR,
1624 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1625 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1630 for (auto reel: cpl->reels()) {
1631 stage("Checking reel", optional<boost::filesystem::path>());
1636 main_picture_active_area,
1643 &have_main_subtitle,
1644 &have_no_main_subtitle,
1645 &most_closed_captions,
1646 &fewest_closed_captions,
1651 verify_text_details(dcp->standard().get_value_or(dcp::Standard::SMPTE), cpl->reels(), notes);
1653 if (dcp->standard() == Standard::SMPTE) {
1654 if (auto msc = cpl->main_sound_configuration()) {
1655 if (state.audio_channels && msc->channels() != *state.audio_channels) {
1657 VerificationNote::Type::ERROR,
1658 VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION,
1659 String::compose("MainSoundConfiguration has %1 channels but sound assets have %2", msc->channels(), *state.audio_channels),
1665 if (have_main_subtitle && have_no_main_subtitle) {
1666 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1669 if (fewest_closed_captions != most_closed_captions) {
1670 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1673 if (cpl->content_kind() == ContentKind::FEATURE) {
1674 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1675 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1677 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1678 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1682 auto ffoc = markers_seen.find(Marker::FFOC);
1683 if (ffoc == markers_seen.end()) {
1684 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1685 } else if (ffoc->second.e != 1) {
1686 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1689 auto lfoc = markers_seen.find(Marker::LFOC);
1690 if (lfoc == markers_seen.end()) {
1691 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1693 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1694 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1695 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1699 LinesCharactersResult result;
1700 for (auto reel: cpl->reels()) {
1701 if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1702 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1706 if (result.line_count_exceeded) {
1707 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1709 if (result.error_length_exceeded) {
1710 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1711 } else if (result.warning_length_exceeded) {
1712 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1715 result = LinesCharactersResult();
1716 for (auto reel: cpl->reels()) {
1717 for (auto i: reel->closed_captions()) {
1719 verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1724 if (result.line_count_exceeded) {
1725 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1727 if (result.error_length_exceeded) {
1728 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1731 if (!cpl->read_composition_metadata()) {
1732 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1733 } else if (!cpl->version_number()) {
1734 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1737 verify_extension_metadata(cpl, notes);
1739 if (cpl->any_encrypted()) {
1740 cxml::Document doc("CompositionPlaylist");
1741 DCP_ASSERT(cpl->file());
1742 doc.read_file(cpl->file().get());
1743 if (!doc.optional_node_child("Signature")) {
1744 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1754 shared_ptr<const DCP> dcp,
1755 shared_ptr<const PKL> pkl,
1756 boost::filesystem::path xsd_dtd_directory,
1757 vector<VerificationNote>& notes
1760 validate_xml(pkl->file().get(), xsd_dtd_directory, notes);
1762 if (pkl_has_encrypted_assets(dcp, pkl)) {
1763 cxml::Document doc("PackingList");
1764 doc.read_file(pkl->file().get());
1765 if (!doc.optional_node_child("Signature")) {
1766 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1770 set<string> uuid_set;
1771 for (auto asset: pkl->assets()) {
1772 if (!uuid_set.insert(asset->id()).second) {
1773 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get()});
1784 shared_ptr<const DCP> dcp,
1785 boost::filesystem::path xsd_dtd_directory,
1786 vector<VerificationNote>& notes
1789 auto asset_map = dcp->asset_map();
1790 DCP_ASSERT(asset_map);
1792 validate_xml(asset_map->file().get(), xsd_dtd_directory, notes);
1794 set<string> uuid_set;
1795 for (auto const& asset: asset_map->assets()) {
1796 if (!uuid_set.insert(asset.id()).second) {
1797 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get()});
1804 vector<VerificationNote>
1806 vector<boost::filesystem::path> directories,
1807 function<void (string, optional<boost::filesystem::path>)> stage,
1808 function<void (float)> progress,
1809 VerificationOptions options,
1810 optional<boost::filesystem::path> xsd_dtd_directory
1813 if (!xsd_dtd_directory) {
1814 xsd_dtd_directory = resources_directory() / "xsd";
1816 *xsd_dtd_directory = boost::filesystem::canonical (*xsd_dtd_directory);
1818 vector<VerificationNote> notes;
1821 vector<shared_ptr<DCP>> dcps;
1822 for (auto i: directories) {
1823 dcps.push_back (make_shared<DCP>(i));
1826 for (auto dcp: dcps) {
1827 stage ("Checking DCP", dcp->directory());
1828 bool carry_on = true;
1830 dcp->read (¬es, true);
1831 } catch (MissingAssetmapError& e) {
1832 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1834 } catch (ReadError& e) {
1835 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1836 } catch (XMLError& e) {
1837 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1838 } catch (MXFFileError& e) {
1839 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1840 } catch (BadURNUUIDError& e) {
1841 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1842 } catch (cxml::Error& e) {
1843 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1850 if (dcp->standard() != Standard::SMPTE) {
1851 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1854 for (auto cpl: dcp->cpls()) {
1867 for (auto pkl: dcp->pkls()) {
1868 stage("Checking PKL", pkl->file());
1869 verify_pkl(dcp, pkl, *xsd_dtd_directory, notes);
1872 if (dcp->asset_map_file()) {
1873 stage("Checking ASSETMAP", dcp->asset_map_file().get());
1874 verify_assetmap(dcp, *xsd_dtd_directory, notes);
1876 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1885 dcp::note_to_string (VerificationNote note)
1887 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1889 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1890 * not "ClosedCaption assets must have an <EntryPoint> tag."
1892 * It's OK to use XML tag names where they are clear.
1893 * If both ID and filename are available, use only the ID.
1894 * End messages with a full stop.
1895 * Messages should not mention whether or not their errors are a part of Bv2.1.
1897 switch (note.code()) {
1898 case VerificationNote::Code::FAILED_READ:
1899 return *note.note();
1900 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1901 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1902 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1903 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1904 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1905 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1906 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1907 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1908 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1909 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1910 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1911 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1912 case VerificationNote::Code::EMPTY_ASSET_PATH:
1913 return "The asset map contains an empty asset path.";
1914 case VerificationNote::Code::MISSING_ASSET:
1915 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1916 case VerificationNote::Code::MISMATCHED_STANDARD:
1917 return "The DCP contains both SMPTE and Interop parts.";
1918 case VerificationNote::Code::INVALID_XML:
1919 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1920 case VerificationNote::Code::MISSING_ASSETMAP:
1921 return "No valid ASSETMAP or ASSETMAP.xml was found.";
1922 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1923 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1924 case VerificationNote::Code::INVALID_DURATION:
1925 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1926 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1927 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());
1928 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1929 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());
1930 case VerificationNote::Code::EXTERNAL_ASSET:
1931 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());
1932 case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1933 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1934 case VerificationNote::Code::INVALID_STANDARD:
1935 return "This DCP does not use the SMPTE standard.";
1936 case VerificationNote::Code::INVALID_LANGUAGE:
1937 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1938 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1939 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1940 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1941 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1942 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1943 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1944 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1945 return "3D 4K DCPs are not allowed.";
1946 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1947 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1948 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1949 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1950 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1951 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());
1952 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1953 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1954 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1955 return "Some subtitle assets have different <Language> tags than others";
1956 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1957 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1958 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1959 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1960 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1961 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1962 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1963 return "At least one subtitle lasts less than 15 frames.";
1964 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1965 return "At least one pair of subtitles is separated by less than 2 frames.";
1966 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1967 return "At least one subtitle extends outside of its reel.";
1968 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1969 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1970 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1971 return "There are more than 52 characters in at least one subtitle line.";
1972 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1973 return "There are more than 79 characters in at least one subtitle line.";
1974 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1975 return "There are more than 3 closed caption lines in at least one place.";
1976 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1977 return "There are more than 32 characters in at least one closed caption line.";
1978 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1979 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1980 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1981 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1982 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1983 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.note().get());
1984 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1985 return "All assets in a reel do not have the same duration.";
1986 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
1987 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
1988 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
1989 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1990 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
1991 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
1992 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
1993 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
1994 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
1995 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
1996 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
1997 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
1998 case VerificationNote::Code::MISSING_HASH:
1999 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
2000 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
2001 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
2002 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
2003 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
2004 case VerificationNote::Code::MISSING_FFOC:
2005 return "There should be a FFOC (first frame of content) marker.";
2006 case VerificationNote::Code::MISSING_LFOC:
2007 return "There should be a LFOC (last frame of content) marker.";
2008 case VerificationNote::Code::INCORRECT_FFOC:
2009 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
2010 case VerificationNote::Code::INCORRECT_LFOC:
2011 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
2012 case VerificationNote::Code::MISSING_CPL_METADATA:
2013 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
2014 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
2015 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
2016 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
2017 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
2018 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
2019 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
2020 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
2021 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
2022 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
2023 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
2024 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
2025 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
2026 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
2027 return "Some assets are encrypted but some are not.";
2028 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
2029 return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1).", note.note().get());
2030 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
2031 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
2032 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
2033 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
2034 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
2035 return "The JPEG2000 tile size is not the same as the image size.";
2036 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
2037 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
2038 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
2039 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
2040 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
2041 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
2042 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
2043 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
2044 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
2045 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
2046 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
2047 return "POC marker found outside main header.";
2048 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
2049 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
2050 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
2051 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
2052 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
2053 return "No TLM marker was found in a JPEG2000 codestream.";
2054 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
2055 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
2056 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
2057 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
2058 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
2060 vector<string> parts;
2061 boost::split (parts, note.note().get(), boost::is_any_of(" "));
2062 DCP_ASSERT (parts.size() == 2);
2063 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]);
2065 case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
2066 return "Some aspect of this DCP could not be checked because it is encrypted.";
2067 case VerificationNote::Code::EMPTY_TEXT:
2068 return "There is an empty <Text> node in a subtitle or closed caption.";
2069 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
2070 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
2071 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
2072 return "Some closed captions are not listed in the order of their vertical position.";
2073 case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
2074 return "There is an <EntryPoint> node inside a <MainMarkers>.";
2075 case VerificationNote::Code::UNEXPECTED_DURATION:
2076 return "There is an <Duration> node inside a <MainMarkers>.";
2077 case VerificationNote::Code::INVALID_CONTENT_KIND:
2078 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
2079 case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
2080 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
2081 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
2082 return String::compose("The PKL %1 has more than one asset with the same ID.", note.note().get());
2083 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
2084 return String::compose("The ASSETMAP %1 has more than one asset with the same ID.", note.note().get());
2085 case VerificationNote::Code::MISSING_SUBTITLE:
2086 return String::compose("The subtitle asset %1 has no subtitles.", note.note().get());
2087 case VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
2088 return String::compose("<IssueDate> has an invalid value: %1", note.note().get());
2089 case VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS:
2090 return String::compose("The sound assets do not all have the same channel count; the first to differ is %1", note.file()->filename());
2091 case VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION:
2092 return String::compose("<MainSoundConfiguration> has an invalid value: %1", note.note().get());
2093 case VerificationNote::Code::MISSING_FONT:
2094 return String::compose("The font file for font ID \"%1\" was not found, or was not referred to in the ASSETMAP.", note.note().get());
2095 case VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE:
2096 return String::compose(
2097 "Frame %1 has an image component that is too large (component %2 is %3 bytes in size).",
2098 note.frame().get(), note.component().get(), note.size().get()
2100 case VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT:
2101 return String::compose("The XML in the subtitle asset %1 has more than one namespace declaration.", note.note().get());
2102 case VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT:
2103 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());
2104 case VerificationNote::Code::MISSING_LOAD_FONT:
2105 return String::compose("The SMPTE subtitle asset %1 has <Text> nodes but no <LoadFont> node", note.id().get());
2106 case VerificationNote::Code::MISMATCHED_ASSET_MAP_ID:
2107 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());
2108 case VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT:
2109 return String::compose("The <LabelText> in a <ContentVersion> in CPL %1 is empty", note.id().get());
2117 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2119 return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
2124 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2126 if (a.type() != b.type()) {
2127 return a.type() < b.type();
2130 if (a.code() != b.code()) {
2131 return a.code() < b.code();
2134 if (a.note() != b.note()) {
2135 return a.note().get_value_or("") < b.note().get_value_or("");
2138 if (a.file() != b.file()) {
2139 return a.file().get_value_or("") < b.file().get_value_or("");
2142 return a.line().get_value_or(0) < b.line().get_value_or(0);
2147 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2149 s << note_to_string (note);
2151 s << " [" << note.note().get() << "]";
2154 s << " [" << note.file().get() << "]";
2157 s << " [" << note.line().get() << "]";