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.
38 #include "reel_closed_caption_asset.h"
39 #include "reel_picture_asset.h"
40 #include "reel_sound_asset.h"
41 #include "reel_subtitle_asset.h"
42 #include "interop_subtitle_asset.h"
43 #include "mono_picture_asset.h"
44 #include "mono_picture_frame.h"
45 #include "stereo_picture_asset.h"
46 #include "stereo_picture_frame.h"
47 #include "exceptions.h"
48 #include "compose.hpp"
49 #include "raw_convert.h"
50 #include "reel_markers_asset.h"
51 #include "smpte_subtitle_asset.h"
52 #include <xercesc/util/PlatformUtils.hpp>
53 #include <xercesc/parsers/XercesDOMParser.hpp>
54 #include <xercesc/parsers/AbstractDOMParser.hpp>
55 #include <xercesc/sax/HandlerBase.hpp>
56 #include <xercesc/dom/DOMImplementation.hpp>
57 #include <xercesc/dom/DOMImplementationLS.hpp>
58 #include <xercesc/dom/DOMImplementationRegistry.hpp>
59 #include <xercesc/dom/DOMLSParser.hpp>
60 #include <xercesc/dom/DOMException.hpp>
61 #include <xercesc/dom/DOMDocument.hpp>
62 #include <xercesc/dom/DOMNodeList.hpp>
63 #include <xercesc/dom/DOMError.hpp>
64 #include <xercesc/dom/DOMLocator.hpp>
65 #include <xercesc/dom/DOMNamedNodeMap.hpp>
66 #include <xercesc/dom/DOMAttr.hpp>
67 #include <xercesc/dom/DOMErrorHandler.hpp>
68 #include <xercesc/framework/LocalFileInputSource.hpp>
69 #include <xercesc/framework/MemBufInputSource.hpp>
70 #include <boost/noncopyable.hpp>
71 #include <boost/algorithm/string.hpp>
83 using std::shared_ptr;
84 using std::make_shared;
85 using boost::optional;
86 using boost::function;
87 using std::dynamic_pointer_cast;
90 using namespace xercesc;
94 xml_ch_to_string (XMLCh const * a)
96 char* x = XMLString::transcode(a);
98 XMLString::release(&x);
102 class XMLValidationError
105 XMLValidationError (SAXParseException const & e)
106 : _message (xml_ch_to_string(e.getMessage()))
107 , _line (e.getLineNumber())
108 , _column (e.getColumnNumber())
109 , _public_id (e.getPublicId() ? xml_ch_to_string(e.getPublicId()) : "")
110 , _system_id (e.getSystemId() ? xml_ch_to_string(e.getSystemId()) : "")
115 string message () const {
119 uint64_t line () const {
123 uint64_t column () const {
127 string public_id () const {
131 string system_id () const {
144 class DCPErrorHandler : public ErrorHandler
147 void warning(const SAXParseException& e)
149 maybe_add (XMLValidationError(e));
152 void error(const SAXParseException& e)
154 maybe_add (XMLValidationError(e));
157 void fatalError(const SAXParseException& e)
159 maybe_add (XMLValidationError(e));
166 list<XMLValidationError> errors () const {
171 void maybe_add (XMLValidationError e)
173 /* XXX: nasty hack */
175 e.message().find("schema document") != string::npos &&
176 e.message().find("has different target namespace from the one specified in instance document") != string::npos
181 _errors.push_back (e);
184 list<XMLValidationError> _errors;
187 class StringToXMLCh : public boost::noncopyable
190 StringToXMLCh (string a)
192 _buffer = XMLString::transcode(a.c_str());
197 XMLString::release (&_buffer);
200 XMLCh const * get () const {
208 class LocalFileResolver : public EntityResolver
211 LocalFileResolver (boost::filesystem::path xsd_dtd_directory)
212 : _xsd_dtd_directory (xsd_dtd_directory)
214 /* XXX: I'm not clear on what things need to be in this list; some XSDs are apparently, magically
215 * found without being here.
217 add("http://www.w3.org/2001/XMLSchema.dtd", "XMLSchema.dtd");
218 add("http://www.w3.org/2001/03/xml.xsd", "xml.xsd");
219 add("http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd", "xmldsig-core-schema.xsd");
220 add("http://www.digicine.com/schemas/437-Y/2007/Main-Stereo-Picture-CPL.xsd", "Main-Stereo-Picture-CPL.xsd");
221 add("http://www.digicine.com/PROTO-ASDCP-CPL-20040511.xsd", "PROTO-ASDCP-CPL-20040511.xsd");
222 add("http://www.digicine.com/PROTO-ASDCP-PKL-20040311.xsd", "PROTO-ASDCP-PKL-20040311.xsd");
223 add("http://www.digicine.com/PROTO-ASDCP-AM-20040311.xsd", "PROTO-ASDCP-AM-20040311.xsd");
224 add("http://www.digicine.com/PROTO-ASDCP-CC-CPL-20070926#", "PROTO-ASDCP-CC-CPL-20070926.xsd");
225 add("interop-subs", "DCSubtitle.v1.mattsson.xsd");
226 add("http://www.smpte-ra.org/schemas/428-7/2010/DCST.xsd", "SMPTE-428-7-2010-DCST.xsd");
227 add("http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata", "SMPTE-429-16.xsd");
228 add("http://www.dolby.com/schemas/2012/AD", "Dolby-2012-AD.xsd");
229 add("http://www.smpte-ra.org/schemas/429-10/2008/Main-Stereo-Picture-CPL", "SMPTE-429-10-2008.xsd");
232 InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id)
237 auto system_id_str = xml_ch_to_string (system_id);
238 auto p = _xsd_dtd_directory;
239 if (_files.find(system_id_str) == _files.end()) {
242 p /= _files[system_id_str];
244 StringToXMLCh ch (p.string());
245 return new LocalFileInputSource(ch.get());
249 void add (string uri, string file)
254 std::map<string, string> _files;
255 boost::filesystem::path _xsd_dtd_directory;
260 parse (XercesDOMParser& parser, boost::filesystem::path xml)
262 parser.parse(xml.string().c_str());
267 parse (XercesDOMParser& parser, string xml)
269 xercesc::MemBufInputSource buf(reinterpret_cast<unsigned char const*>(xml.c_str()), xml.size(), "");
276 validate_xml (T xml, boost::filesystem::path xsd_dtd_directory, vector<VerificationNote>& notes)
279 XMLPlatformUtils::Initialize ();
280 } catch (XMLException& e) {
281 throw MiscError ("Failed to initialise xerces library");
284 DCPErrorHandler error_handler;
286 /* All the xerces objects in this scope must be destroyed before XMLPlatformUtils::Terminate() is called */
288 XercesDOMParser parser;
289 parser.setValidationScheme(XercesDOMParser::Val_Always);
290 parser.setDoNamespaces(true);
291 parser.setDoSchema(true);
293 vector<string> schema;
294 schema.push_back("xml.xsd");
295 schema.push_back("xmldsig-core-schema.xsd");
296 schema.push_back("SMPTE-429-7-2006-CPL.xsd");
297 schema.push_back("SMPTE-429-8-2006-PKL.xsd");
298 schema.push_back("SMPTE-429-9-2007-AM.xsd");
299 schema.push_back("Main-Stereo-Picture-CPL.xsd");
300 schema.push_back("PROTO-ASDCP-CPL-20040511.xsd");
301 schema.push_back("PROTO-ASDCP-PKL-20040311.xsd");
302 schema.push_back("PROTO-ASDCP-AM-20040311.xsd");
303 schema.push_back("DCSubtitle.v1.mattsson.xsd");
304 schema.push_back("DCDMSubtitle-2010.xsd");
305 schema.push_back("PROTO-ASDCP-CC-CPL-20070926.xsd");
306 schema.push_back("SMPTE-429-16.xsd");
307 schema.push_back("Dolby-2012-AD.xsd");
308 schema.push_back("SMPTE-429-10-2008.xsd");
309 schema.push_back("xlink.xsd");
310 schema.push_back("SMPTE-335-2012.xsd");
311 schema.push_back("SMPTE-395-2014-13-1-aaf.xsd");
312 schema.push_back("isdcf-mca.xsd");
313 schema.push_back("SMPTE-429-12-2008.xsd");
315 /* XXX: I'm not especially clear what this is for, but it seems to be necessary.
316 * Schemas that are not mentioned in this list are not read, and the things
317 * they describe are not checked.
320 for (auto i: schema) {
321 locations += String::compose("%1 %1 ", i, i);
324 parser.setExternalSchemaLocation(locations.c_str());
325 parser.setValidationSchemaFullChecking(true);
326 parser.setErrorHandler(&error_handler);
328 LocalFileResolver resolver (xsd_dtd_directory);
329 parser.setEntityResolver(&resolver);
332 parser.resetDocumentPool();
334 } catch (XMLException& e) {
335 throw MiscError(xml_ch_to_string(e.getMessage()));
336 } catch (DOMException& e) {
337 throw MiscError(xml_ch_to_string(e.getMessage()));
339 throw MiscError("Unknown exception from xerces");
343 XMLPlatformUtils::Terminate ();
345 for (auto i: error_handler.errors()) {
348 VerificationNote::VERIFY_ERROR,
349 VerificationNote::XML_VALIDATION_ERROR,
351 boost::trim_copy(i.public_id() + " " + i.system_id()),
359 enum VerifyAssetResult {
360 VERIFY_ASSET_RESULT_GOOD,
361 VERIFY_ASSET_RESULT_CPL_PKL_DIFFER,
362 VERIFY_ASSET_RESULT_BAD
366 static VerifyAssetResult
367 verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelMXF> reel_mxf, function<void (float)> progress)
369 auto const actual_hash = reel_mxf->asset_ref()->hash(progress);
371 auto pkls = dcp->pkls();
372 /* We've read this DCP in so it must have at least one PKL */
373 DCP_ASSERT (!pkls.empty());
375 auto asset = reel_mxf->asset_ref().asset();
377 optional<string> pkl_hash;
379 pkl_hash = i->hash (reel_mxf->asset_ref()->id());
385 DCP_ASSERT (pkl_hash);
387 auto cpl_hash = reel_mxf->hash();
388 if (cpl_hash && *cpl_hash != *pkl_hash) {
389 return VERIFY_ASSET_RESULT_CPL_PKL_DIFFER;
392 if (actual_hash != *pkl_hash) {
393 return VERIFY_ASSET_RESULT_BAD;
396 return VERIFY_ASSET_RESULT_GOOD;
401 verify_language_tag (string tag, vector<VerificationNote>& notes)
404 dcp::LanguageTag test (tag);
405 } catch (dcp::LanguageTagError &) {
406 notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::BAD_LANGUAGE, tag));
411 enum VerifyPictureAssetResult
413 VERIFY_PICTURE_ASSET_RESULT_GOOD,
414 VERIFY_PICTURE_ASSET_RESULT_FRAME_NEARLY_TOO_LARGE,
415 VERIFY_PICTURE_ASSET_RESULT_BAD,
420 biggest_frame_size (shared_ptr<const MonoPictureFrame> frame)
422 return frame->size ();
426 biggest_frame_size (shared_ptr<const StereoPictureFrame> frame)
428 return max(frame->left()->size(), frame->right()->size());
432 template <class A, class R, class F>
433 optional<VerifyPictureAssetResult>
434 verify_picture_asset_type (shared_ptr<const ReelMXF> reel_mxf, function<void (float)> progress)
436 auto asset = dynamic_pointer_cast<A>(reel_mxf->asset_ref().asset());
438 return optional<VerifyPictureAssetResult>();
441 int biggest_frame = 0;
442 auto reader = asset->start_read ();
443 auto const duration = asset->intrinsic_duration ();
444 for (int64_t i = 0; i < duration; ++i) {
445 shared_ptr<const F> frame = reader->get_frame (i);
446 biggest_frame = max(biggest_frame, biggest_frame_size(frame));
447 progress (float(i) / duration);
450 static const int max_frame = rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
451 static const int risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
452 if (biggest_frame > max_frame) {
453 return VERIFY_PICTURE_ASSET_RESULT_BAD;
454 } else if (biggest_frame > risky_frame) {
455 return VERIFY_PICTURE_ASSET_RESULT_FRAME_NEARLY_TOO_LARGE;
458 return VERIFY_PICTURE_ASSET_RESULT_GOOD;
462 static VerifyPictureAssetResult
463 verify_picture_asset (shared_ptr<const ReelMXF> reel_mxf, function<void (float)> progress)
465 auto r = verify_picture_asset_type<MonoPictureAsset, MonoPictureAssetReader, MonoPictureFrame>(reel_mxf, progress);
467 r = verify_picture_asset_type<StereoPictureAsset, StereoPictureAssetReader, StereoPictureFrame>(reel_mxf, progress);
476 verify_main_picture_asset (
477 shared_ptr<const DCP> dcp,
478 shared_ptr<const ReelPictureAsset> reel_asset,
479 function<void (string, optional<boost::filesystem::path>)> stage,
480 function<void (float)> progress,
481 vector<VerificationNote>& notes
484 auto asset = reel_asset->asset();
485 auto const file = *asset->file();
486 stage ("Checking picture asset hash", file);
487 auto const r = verify_asset (dcp, reel_asset, progress);
489 case VERIFY_ASSET_RESULT_BAD:
492 VerificationNote::VERIFY_ERROR, VerificationNote::PICTURE_HASH_INCORRECT, file
496 case VERIFY_ASSET_RESULT_CPL_PKL_DIFFER:
499 VerificationNote::VERIFY_ERROR, VerificationNote::PKL_CPL_PICTURE_HASHES_DIFFER, file
506 stage ("Checking picture frame sizes", asset->file());
507 auto const pr = verify_picture_asset (reel_asset, progress);
509 case VERIFY_PICTURE_ASSET_RESULT_BAD:
512 VerificationNote::VERIFY_ERROR, VerificationNote::PICTURE_FRAME_TOO_LARGE_IN_BYTES, file
516 case VERIFY_PICTURE_ASSET_RESULT_FRAME_NEARLY_TOO_LARGE:
519 VerificationNote::VERIFY_WARNING, VerificationNote::PICTURE_FRAME_NEARLY_TOO_LARGE_IN_BYTES, file
527 /* Only flat/scope allowed by Bv2.1 */
529 asset->size() != dcp::Size(2048, 858) &&
530 asset->size() != dcp::Size(1998, 1080) &&
531 asset->size() != dcp::Size(4096, 1716) &&
532 asset->size() != dcp::Size(3996, 2160)) {
535 VerificationNote::VERIFY_BV21_ERROR,
536 VerificationNote::PICTURE_ASSET_INVALID_SIZE_IN_PIXELS,
537 String::compose("%1x%2", asset->size().width, asset->size().height),
543 /* Only 24, 25, 48fps allowed for 2K */
545 (asset->size() == dcp::Size(2048, 858) || asset->size() == dcp::Size(1998, 1080)) &&
546 (asset->edit_rate() != dcp::Fraction(24, 1) && asset->edit_rate() != dcp::Fraction(25, 1) && asset->edit_rate() != dcp::Fraction(48, 1))
550 VerificationNote::VERIFY_BV21_ERROR,
551 VerificationNote::PICTURE_ASSET_INVALID_FRAME_RATE_FOR_2K,
552 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
558 if (asset->size() == dcp::Size(4096, 1716) || asset->size() == dcp::Size(3996, 2160)) {
559 /* Only 24fps allowed for 4K */
560 if (asset->edit_rate() != dcp::Fraction(24, 1)) {
563 VerificationNote::VERIFY_BV21_ERROR,
564 VerificationNote::PICTURE_ASSET_INVALID_FRAME_RATE_FOR_4K,
565 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
571 /* Only 2D allowed for 4K */
572 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
575 VerificationNote::VERIFY_BV21_ERROR,
576 VerificationNote::PICTURE_ASSET_4K_3D,
588 verify_main_sound_asset (
589 shared_ptr<const DCP> dcp,
590 shared_ptr<const ReelSoundAsset> reel_asset,
591 function<void (string, optional<boost::filesystem::path>)> stage,
592 function<void (float)> progress,
593 vector<VerificationNote>& notes
596 auto asset = reel_asset->asset();
597 stage ("Checking sound asset hash", asset->file());
598 auto const r = verify_asset (dcp, reel_asset, progress);
600 case VERIFY_ASSET_RESULT_BAD:
603 VerificationNote::VERIFY_ERROR, VerificationNote::SOUND_HASH_INCORRECT, *asset->file()
607 case VERIFY_ASSET_RESULT_CPL_PKL_DIFFER:
610 VerificationNote::VERIFY_ERROR, VerificationNote::PKL_CPL_SOUND_HASHES_DIFFER, *asset->file()
618 stage ("Checking sound asset metadata", asset->file());
620 verify_language_tag (asset->language(), notes);
621 if (asset->sampling_rate() != 48000) {
624 VerificationNote::VERIFY_BV21_ERROR, VerificationNote::INVALID_SOUND_FRAME_RATE, *asset->file()
632 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
634 /* XXX: is Language compulsory? */
635 if (reel_asset->language()) {
636 verify_language_tag (*reel_asset->language(), notes);
639 if (!reel_asset->entry_point()) {
640 notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_SUBTITLE_ENTRY_POINT });
641 } else if (reel_asset->entry_point().get()) {
642 notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::SUBTITLE_ENTRY_POINT_NON_ZERO });
648 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
650 /* XXX: is Language compulsory? */
651 if (reel_asset->language()) {
652 verify_language_tag (*reel_asset->language(), notes);
655 if (!reel_asset->entry_point()) {
656 notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_CLOSED_CAPTION_ENTRY_POINT });
657 } else if (reel_asset->entry_point().get()) {
658 notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::CLOSED_CAPTION_ENTRY_POINT_NON_ZERO });
665 boost::optional<string> subtitle_language;
671 verify_smpte_subtitle_asset (
672 shared_ptr<const dcp::SMPTESubtitleAsset> asset,
673 vector<VerificationNote>& notes,
677 if (asset->language()) {
678 auto const language = *asset->language();
679 verify_language_tag (language, notes);
680 if (!state.subtitle_language) {
681 state.subtitle_language = language;
682 } else if (state.subtitle_language != language) {
685 VerificationNote::VERIFY_BV21_ERROR, VerificationNote::SUBTITLE_LANGUAGES_DIFFER, *asset->file()
692 VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_SUBTITLE_LANGUAGE, *asset->file()
696 if (boost::filesystem::file_size(*asset->file()) > 115 * 1024 * 1024) {
699 VerificationNote::VERIFY_BV21_ERROR, VerificationNote::TIMED_TEXT_ASSET_TOO_LARGE_IN_BYTES, *asset->file()
703 /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
704 * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
706 auto fonts = asset->font_data ();
708 for (auto i: fonts) {
709 total_size += i.second.size();
711 if (total_size > 10 * 1024 * 1024) {
714 VerificationNote::VERIFY_BV21_ERROR, VerificationNote::TIMED_TEXT_FONTS_TOO_LARGE_IN_BYTES, *asset->file()
719 if (!asset->start_time()) {
722 VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_SUBTITLE_START_TIME, *asset->file())
724 } else if (asset->start_time() != dcp::Time()) {
727 VerificationNote::VERIFY_BV21_ERROR, VerificationNote::SUBTITLE_START_TIME_NON_ZERO, *asset->file())
734 verify_subtitle_asset (
735 shared_ptr<const SubtitleAsset> asset,
736 function<void (string, optional<boost::filesystem::path>)> stage,
737 boost::filesystem::path xsd_dtd_directory,
738 vector<VerificationNote>& notes,
742 stage ("Checking subtitle XML", asset->file());
743 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
744 * gets passed through libdcp which may clean up and therefore hide errors.
746 validate_xml (asset->raw_xml(), xsd_dtd_directory, notes);
748 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
750 verify_smpte_subtitle_asset (smpte, notes, state);
756 verify_closed_caption_asset (
757 shared_ptr<const SubtitleAsset> asset,
758 function<void (string, optional<boost::filesystem::path>)> stage,
759 boost::filesystem::path xsd_dtd_directory,
760 vector<VerificationNote>& notes,
764 verify_subtitle_asset (asset, stage, xsd_dtd_directory, notes, state);
766 if (asset->raw_xml().size() > 256 * 1024) {
769 VerificationNote::VERIFY_BV21_ERROR, VerificationNote::CLOSED_CAPTION_XML_TOO_LARGE_IN_BYTES, *asset->file()
779 vector<shared_ptr<dcp::Reel>> reels,
780 optional<int> picture_frame_rate,
781 vector<VerificationNote>& notes,
782 std::function<bool (shared_ptr<dcp::Reel>)> check,
783 std::function<string (shared_ptr<dcp::Reel>)> xml,
784 std::function<int64_t (shared_ptr<dcp::Reel>)> duration
787 /* end of last subtitle (in editable units) */
788 optional<int64_t> last_out;
789 auto too_short = false;
790 auto too_close = false;
791 auto too_early = false;
792 /* current reel start time (in editable units) */
793 int64_t reel_offset = 0;
795 std::function<void (cxml::ConstNodePtr, int, int, bool)> parse;
796 parse = [&parse, &last_out, &too_short, &too_close, &too_early, &reel_offset](cxml::ConstNodePtr node, int tcr, int pfr, bool first_reel) {
797 if (node->name() == "Subtitle") {
798 dcp::Time in (node->string_attribute("TimeIn"), tcr);
799 dcp::Time out (node->string_attribute("TimeOut"), tcr);
800 if (first_reel && in < dcp::Time(0, 0, 4, 0, tcr)) {
803 auto length = out - in;
804 if (length.as_editable_units(pfr) < 15) {
808 /* XXX: this feels dubious - is it really what Bv2.1 means? */
809 auto distance = reel_offset + in.as_editable_units(pfr) - *last_out;
810 if (distance >= 0 && distance < 2) {
814 last_out = reel_offset + out.as_editable_units(pfr);
816 for (auto i: node->node_children()) {
817 parse(i, tcr, pfr, first_reel);
822 for (auto i = 0U; i < reels.size(); ++i) {
823 if (!check(reels[i])) {
827 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
828 * read in by libdcp's parser.
831 auto doc = make_shared<cxml::Document>("SubtitleReel");
832 doc->read_string (xml(reels[i]));
833 auto const tcr = doc->number_child<int>("TimeCodeRate");
834 parse (doc, tcr, picture_frame_rate.get_value_or(24), i == 0);
835 reel_offset += duration(reels[i]);
841 VerificationNote::VERIFY_WARNING, VerificationNote::FIRST_TEXT_TOO_EARLY
849 VerificationNote::VERIFY_WARNING, VerificationNote::SUBTITLE_TOO_SHORT
857 VerificationNote::VERIFY_WARNING, VerificationNote::SUBTITLE_TOO_CLOSE
864 struct LinesCharactersResult
866 bool warning_length_exceeded = false;
867 bool error_length_exceeded = false;
868 bool line_count_exceeded = false;
874 check_text_lines_and_characters (
875 shared_ptr<SubtitleAsset> asset,
878 LinesCharactersResult* result
884 Event (dcp::Time time_, float position_, int characters_)
886 , position (position_)
887 , characters (characters_)
890 Event (dcp::Time time_, shared_ptr<Event> start_)
896 int position; //< position from 0 at top of screen to 100 at bottom
898 shared_ptr<Event> start;
901 vector<shared_ptr<Event>> events;
903 auto position = [](shared_ptr<const SubtitleString> sub) {
904 switch (sub->v_align()) {
906 return lrintf(sub->v_position() * 100);
908 return lrintf((0.5f + sub->v_position()) * 100);
910 return lrintf((1.0f - sub->v_position()) * 100);
916 for (auto j: asset->subtitles()) {
917 auto text = dynamic_pointer_cast<const SubtitleString>(j);
919 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
920 events.push_back(in);
921 events.push_back(make_shared<Event>(text->out(), in));
925 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
926 return a->time < b->time;
929 map<int, int> current;
930 for (auto i: events) {
931 if (current.size() > 3) {
932 result->line_count_exceeded = true;
934 for (auto j: current) {
935 if (j.second >= warning_length) {
936 result->warning_length_exceeded = true;
938 if (j.second >= error_length) {
939 result->error_length_exceeded = true;
944 /* end of a subtitle */
945 DCP_ASSERT (current.find(i->start->position) != current.end());
946 if (current[i->start->position] == i->start->characters) {
947 current.erase(i->start->position);
949 current[i->start->position] -= i->start->characters;
952 /* start of a subtitle */
953 if (current.find(i->position) == current.end()) {
954 current[i->position] = i->characters;
956 current[i->position] += i->characters;
965 check_text_timing (vector<shared_ptr<dcp::Reel>> reels, vector<VerificationNote>& notes)
971 optional<int> picture_frame_rate;
972 if (reels[0]->main_picture()) {
973 picture_frame_rate = reels[0]->main_picture()->frame_rate().numerator;
976 if (reels[0]->main_subtitle()) {
977 check_text_timing (reels, picture_frame_rate, notes,
978 [](shared_ptr<dcp::Reel> reel) {
979 return static_cast<bool>(reel->main_subtitle());
981 [](shared_ptr<dcp::Reel> reel) {
982 return reel->main_subtitle()->asset()->raw_xml();
984 [](shared_ptr<dcp::Reel> reel) {
985 return reel->main_subtitle()->actual_duration();
990 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
991 check_text_timing (reels, picture_frame_rate, notes,
992 [i](shared_ptr<dcp::Reel> reel) {
993 return i < reel->closed_captions().size();
995 [i](shared_ptr<dcp::Reel> reel) {
996 return reel->closed_captions()[i]->asset()->raw_xml();
998 [i](shared_ptr<dcp::Reel> reel) {
999 return reel->closed_captions()[i]->actual_duration();
1006 vector<VerificationNote>
1008 vector<boost::filesystem::path> directories,
1009 function<void (string, optional<boost::filesystem::path>)> stage,
1010 function<void (float)> progress,
1011 boost::filesystem::path xsd_dtd_directory
1014 xsd_dtd_directory = boost::filesystem::canonical (xsd_dtd_directory);
1016 vector<VerificationNote> notes;
1019 vector<shared_ptr<DCP>> dcps;
1020 for (auto i: directories) {
1021 dcps.push_back (shared_ptr<DCP> (new DCP (i)));
1024 for (auto dcp: dcps) {
1025 stage ("Checking DCP", dcp->directory());
1028 } catch (ReadError& e) {
1029 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::GENERAL_READ, string(e.what())));
1030 } catch (XMLError& e) {
1031 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::GENERAL_READ, string(e.what())));
1032 } catch (MXFFileError& e) {
1033 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::GENERAL_READ, string(e.what())));
1034 } catch (cxml::Error& e) {
1035 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::GENERAL_READ, string(e.what())));
1038 if (dcp->standard() != dcp::SMPTE) {
1039 notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::NOT_SMPTE));
1042 for (auto cpl: dcp->cpls()) {
1043 stage ("Checking CPL", cpl->file());
1044 validate_xml (cpl->file().get(), xsd_dtd_directory, notes);
1046 for (auto const& i: cpl->additional_subtitle_languages()) {
1047 verify_language_tag (i, notes);
1050 if (cpl->release_territory()) {
1051 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") {
1052 auto terr = cpl->release_territory().get();
1053 /* Must be a valid region tag, or "001" */
1055 LanguageTag::RegionSubtag test (terr);
1057 if (terr != "001") {
1058 notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::BAD_LANGUAGE, terr});
1064 if (dcp->standard() == dcp::SMPTE) {
1065 if (!cpl->annotation_text()) {
1066 notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_ANNOTATION_TEXT_IN_CPL));
1067 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1068 notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::CPL_ANNOTATION_TEXT_DIFFERS_FROM_CONTENT_TITLE_TEXT));
1072 /* Check that the CPL's hash corresponds to the PKL */
1073 for (auto i: dcp->pkls()) {
1074 optional<string> h = i->hash(cpl->id());
1075 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1076 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::CPL_HASH_INCORRECT));
1080 /* set to true if any reel has a MainSubtitle */
1081 auto have_main_subtitle = false;
1082 /* set to true if any reel has no MainSubtitle */
1083 auto have_no_main_subtitle = false;
1084 /* fewest number of closed caption assets seen in a reel */
1085 size_t fewest_closed_captions = SIZE_MAX;
1086 /* most number of closed caption assets seen in a reel */
1087 size_t most_closed_captions = 0;
1088 map<Marker, Time> markers_seen;
1090 for (auto reel: cpl->reels()) {
1091 stage ("Checking reel", optional<boost::filesystem::path>());
1093 for (auto i: reel->assets()) {
1094 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1095 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::DURATION_TOO_SMALL, i->id()));
1097 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1098 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::INTRINSIC_DURATION_TOO_SMALL, i->id()));
1100 auto mxf = dynamic_pointer_cast<ReelMXF>(i);
1101 if (mxf && !mxf->hash()) {
1102 notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_HASH, i->id()});
1106 if (dcp->standard() == dcp::SMPTE) {
1107 boost::optional<int64_t> duration;
1108 for (auto i: reel->assets()) {
1110 duration = i->actual_duration();
1111 } else if (*duration != i->actual_duration()) {
1112 notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISMATCHED_ASSET_DURATION, i->id()));
1118 if (reel->main_picture()) {
1119 /* Check reel stuff */
1120 auto const frame_rate = reel->main_picture()->frame_rate();
1121 if (frame_rate.denominator != 1 ||
1122 (frame_rate.numerator != 24 &&
1123 frame_rate.numerator != 25 &&
1124 frame_rate.numerator != 30 &&
1125 frame_rate.numerator != 48 &&
1126 frame_rate.numerator != 50 &&
1127 frame_rate.numerator != 60 &&
1128 frame_rate.numerator != 96)) {
1129 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::INVALID_PICTURE_FRAME_RATE));
1132 if (reel->main_picture()->asset_ref().resolved()) {
1133 verify_main_picture_asset (dcp, reel->main_picture(), stage, progress, notes);
1137 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1138 verify_main_sound_asset (dcp, reel->main_sound(), stage, progress, notes);
1141 if (reel->main_subtitle()) {
1142 verify_main_subtitle_reel (reel->main_subtitle(), notes);
1143 if (reel->main_subtitle()->asset_ref().resolved()) {
1144 verify_subtitle_asset (reel->main_subtitle()->asset(), stage, xsd_dtd_directory, notes, state);
1146 have_main_subtitle = true;
1148 have_no_main_subtitle = true;
1151 for (auto i: reel->closed_captions()) {
1152 verify_closed_caption_reel (i, notes);
1153 if (i->asset_ref().resolved()) {
1154 verify_closed_caption_asset (i->asset(), stage, xsd_dtd_directory, notes, state);
1158 if (reel->main_markers()) {
1159 for (auto const& i: reel->main_markers()->get()) {
1160 markers_seen.insert (i);
1164 fewest_closed_captions = std::min (fewest_closed_captions, reel->closed_captions().size());
1165 most_closed_captions = std::max (most_closed_captions, reel->closed_captions().size());
1168 if (dcp->standard() == dcp::SMPTE) {
1170 if (have_main_subtitle && have_no_main_subtitle) {
1171 notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MAIN_SUBTITLE_NOT_IN_ALL_REELS});
1174 if (fewest_closed_captions != most_closed_captions) {
1175 notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::CLOSED_CAPTION_ASSET_COUNTS_DIFFER});
1178 if (cpl->content_kind() == FEATURE) {
1179 if (markers_seen.find(dcp::Marker::FFEC) == markers_seen.end()) {
1180 notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_FFEC_IN_FEATURE});
1182 if (markers_seen.find(dcp::Marker::FFMC) == markers_seen.end()) {
1183 notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_FFMC_IN_FEATURE});
1187 auto ffoc = markers_seen.find(dcp::Marker::FFOC);
1188 if (ffoc == markers_seen.end()) {
1189 notes.push_back ({VerificationNote::VERIFY_WARNING, VerificationNote::MISSING_FFOC});
1190 } else if (ffoc->second.e != 1) {
1191 notes.push_back ({VerificationNote::VERIFY_WARNING, VerificationNote::INCORRECT_FFOC});
1194 auto lfoc = markers_seen.find(dcp::Marker::LFOC);
1195 if (lfoc == markers_seen.end()) {
1196 notes.push_back ({VerificationNote::VERIFY_WARNING, VerificationNote::MISSING_LFOC});
1197 } else if (lfoc->second.as_editable_units(lfoc->second.tcr) != (cpl->reels().back()->duration() - 1)) {
1198 notes.push_back ({VerificationNote::VERIFY_WARNING, VerificationNote::INCORRECT_LFOC});
1201 check_text_timing (cpl->reels(), notes);
1203 LinesCharactersResult result;
1204 for (auto reel: cpl->reels()) {
1205 if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1206 check_text_lines_and_characters (reel->main_subtitle()->asset(), 52, 79, &result);
1210 if (result.line_count_exceeded) {
1211 notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::TOO_MANY_SUBTITLE_LINES));
1213 if (result.error_length_exceeded) {
1214 notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::SUBTITLE_LINE_TOO_LONG));
1215 } else if (result.warning_length_exceeded) {
1216 notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::SUBTITLE_LINE_LONGER_THAN_RECOMMENDED));
1219 result = LinesCharactersResult();
1220 for (auto reel: cpl->reels()) {
1221 for (auto i: reel->closed_captions()) {
1223 check_text_lines_and_characters (i->asset(), 32, 32, &result);
1228 if (result.line_count_exceeded) {
1229 notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::TOO_MANY_CLOSED_CAPTION_LINES));
1231 if (result.error_length_exceeded) {
1232 notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::CLOSED_CAPTION_LINE_TOO_LONG));
1237 for (auto pkl: dcp->pkls()) {
1238 stage ("Checking PKL", pkl->file());
1239 validate_xml (pkl->file().get(), xsd_dtd_directory, notes);
1242 if (dcp->asset_map_path()) {
1243 stage ("Checking ASSETMAP", dcp->asset_map_path().get());
1244 validate_xml (dcp->asset_map_path().get(), xsd_dtd_directory, notes);
1246 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::MISSING_ASSETMAP));
1254 dcp::note_to_string (dcp::VerificationNote note)
1256 switch (note.code()) {
1257 case dcp::VerificationNote::GENERAL_READ:
1258 return *note.note();
1259 case dcp::VerificationNote::CPL_HASH_INCORRECT:
1260 return "The hash of the CPL in the PKL does not agree with the CPL file.";
1261 case dcp::VerificationNote::INVALID_PICTURE_FRAME_RATE:
1262 return "The picture in a reel has an invalid frame rate.";
1263 case dcp::VerificationNote::PICTURE_HASH_INCORRECT:
1264 return dcp::String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1265 case dcp::VerificationNote::PKL_CPL_PICTURE_HASHES_DIFFER:
1266 return dcp::String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1267 case dcp::VerificationNote::SOUND_HASH_INCORRECT:
1268 return dcp::String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1269 case dcp::VerificationNote::PKL_CPL_SOUND_HASHES_DIFFER:
1270 return dcp::String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1271 case dcp::VerificationNote::EMPTY_ASSET_PATH:
1272 return "The asset map contains an empty asset path.";
1273 case dcp::VerificationNote::MISSING_ASSET:
1274 return String::compose("The file for an asset in the asset map cannot be found; missing file is %1.", note.file()->filename());
1275 case dcp::VerificationNote::MISMATCHED_STANDARD:
1276 return "The DCP contains both SMPTE and Interop parts.";
1277 case dcp::VerificationNote::XML_VALIDATION_ERROR:
1278 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1279 case dcp::VerificationNote::MISSING_ASSETMAP:
1280 return "No ASSETMAP or ASSETMAP.xml was found.";
1281 case dcp::VerificationNote::INTRINSIC_DURATION_TOO_SMALL:
1282 return String::compose("The intrinsic duration of an asset is less than 1 second long: %1", note.note().get());
1283 case dcp::VerificationNote::DURATION_TOO_SMALL:
1284 return String::compose("The duration of an asset is less than 1 second long: %1", note.note().get());
1285 case dcp::VerificationNote::PICTURE_FRAME_TOO_LARGE_IN_BYTES:
1286 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());
1287 case dcp::VerificationNote::PICTURE_FRAME_NEARLY_TOO_LARGE_IN_BYTES:
1288 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());
1289 case dcp::VerificationNote::EXTERNAL_ASSET:
1290 return String::compose("An asset that this DCP refers to is not included in the DCP. It may be a VF. Missing asset is %1.", note.note().get());
1291 case dcp::VerificationNote::NOT_SMPTE:
1292 return "This DCP does not use the SMPTE standard, which is required for Bv2.1 compliance.";
1293 case dcp::VerificationNote::BAD_LANGUAGE:
1294 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1295 case dcp::VerificationNote::PICTURE_ASSET_INVALID_SIZE_IN_PIXELS:
1296 return String::compose("A picture asset's size (%1) is not one of those allowed by Bv2.1 (2048x858, 1998x1080, 4096x1716 or 3996x2160)", note.note().get());
1297 case dcp::VerificationNote::PICTURE_ASSET_INVALID_FRAME_RATE_FOR_2K:
1298 return String::compose("A picture asset's frame rate (%1) is not one of those allowed for 2K DCPs by Bv2.1 (24, 25 or 48fps)", note.note().get());
1299 case dcp::VerificationNote::PICTURE_ASSET_INVALID_FRAME_RATE_FOR_4K:
1300 return String::compose("A picture asset's frame rate (%1) is not 24fps as required for 4K DCPs by Bv2.1", note.note().get());
1301 case dcp::VerificationNote::PICTURE_ASSET_4K_3D:
1302 return "3D 4K DCPs are not allowed by Bv2.1";
1303 case dcp::VerificationNote::CLOSED_CAPTION_XML_TOO_LARGE_IN_BYTES:
1304 return String::compose("The XML for the closed caption asset %1 is longer than the 256KB maximum required by Bv2.1", note.file()->filename());
1305 case dcp::VerificationNote::TIMED_TEXT_ASSET_TOO_LARGE_IN_BYTES:
1306 return String::compose("The total size of the timed text asset %1 is larger than the 115MB maximum required by Bv2.1", note.file()->filename());
1307 case dcp::VerificationNote::TIMED_TEXT_FONTS_TOO_LARGE_IN_BYTES:
1308 return String::compose("The total size of the fonts in timed text asset %1 is larger than the 10MB maximum required by Bv2.1", note.file()->filename());
1309 case dcp::VerificationNote::MISSING_SUBTITLE_LANGUAGE:
1310 return String::compose("The XML for a SMPTE subtitle asset has no <Language> tag, which is required by Bv2.1", note.file()->filename());
1311 case dcp::VerificationNote::SUBTITLE_LANGUAGES_DIFFER:
1312 return String::compose("Some subtitle assets have different <Language> tags than others", note.file()->filename());
1313 case dcp::VerificationNote::MISSING_SUBTITLE_START_TIME:
1314 return String::compose("The XML for a SMPTE subtitle asset has no <StartTime> tag, which is required by Bv2.1", note.file()->filename());
1315 case dcp::VerificationNote::SUBTITLE_START_TIME_NON_ZERO:
1316 return String::compose("The XML for a SMPTE subtitle asset has a non-zero <StartTime> tag, which is disallowed by Bv2.1", note.file()->filename());
1317 case dcp::VerificationNote::FIRST_TEXT_TOO_EARLY:
1318 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1319 case dcp::VerificationNote::SUBTITLE_TOO_SHORT:
1320 return "At least one subtitle is less than the minimum of 15 frames suggested by Bv2.1";
1321 case dcp::VerificationNote::SUBTITLE_TOO_CLOSE:
1322 return "At least one pair of subtitles are separated by less than the the minimum of 2 frames suggested by Bv2.1";
1323 case dcp::VerificationNote::TOO_MANY_SUBTITLE_LINES:
1324 return "There are more than 3 subtitle lines in at least one place in the DCP, which Bv2.1 advises against.";
1325 case dcp::VerificationNote::SUBTITLE_LINE_LONGER_THAN_RECOMMENDED:
1326 return "There are more than 52 characters in at least one subtitle line, which Bv2.1 advises against.";
1327 case dcp::VerificationNote::SUBTITLE_LINE_TOO_LONG:
1328 return "There are more than 79 characters in at least one subtitle line, which Bv2.1 strongly advises against.";
1329 case dcp::VerificationNote::TOO_MANY_CLOSED_CAPTION_LINES:
1330 return "There are more than 3 closed caption lines in at least one place, which is disallowed by Bv2.1";
1331 case dcp::VerificationNote::CLOSED_CAPTION_LINE_TOO_LONG:
1332 return "There are more than 32 characters in at least one closed caption line, which is disallowed by Bv2.1";
1333 case dcp::VerificationNote::INVALID_SOUND_FRAME_RATE:
1334 return "A sound asset has a sampling rate other than 48kHz, which is disallowed by Bv2.1";
1335 case dcp::VerificationNote::MISSING_ANNOTATION_TEXT_IN_CPL:
1336 return "The CPL has no <AnnotationText> tag, which is required by Bv2.1";
1337 case dcp::VerificationNote::CPL_ANNOTATION_TEXT_DIFFERS_FROM_CONTENT_TITLE_TEXT:
1338 return "The CPL's <AnnotationText> differs from its <ContentTitleText>, which Bv2.1 advises against.";
1339 case dcp::VerificationNote::MISMATCHED_ASSET_DURATION:
1340 return "All assets in a reel do not have the same duration, which is required by Bv2.1";
1341 case dcp::VerificationNote::MAIN_SUBTITLE_NOT_IN_ALL_REELS:
1342 return "At least one reel contains a subtitle asset, but some reel(s) do not";
1343 case dcp::VerificationNote::CLOSED_CAPTION_ASSET_COUNTS_DIFFER:
1344 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1345 case dcp::VerificationNote::MISSING_SUBTITLE_ENTRY_POINT:
1346 return "Subtitle assets must have an <EntryPoint> tag.";
1347 case dcp::VerificationNote::SUBTITLE_ENTRY_POINT_NON_ZERO:
1348 return "Subtitle assets must have an <EntryPoint> of 0.";
1349 case dcp::VerificationNote::MISSING_CLOSED_CAPTION_ENTRY_POINT:
1350 return "Closed caption assets must have an <EntryPoint> tag.";
1351 case dcp::VerificationNote::CLOSED_CAPTION_ENTRY_POINT_NON_ZERO:
1352 return "Closed caption assets must have an <EntryPoint> of 0.";
1353 case dcp::VerificationNote::MISSING_HASH:
1354 return String::compose("An asset is missing a <Hash> tag: %1", note.note().get());
1355 case dcp::VerificationNote::MISSING_FFEC_IN_FEATURE:
1356 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker";
1357 case dcp::VerificationNote::MISSING_FFMC_IN_FEATURE:
1358 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker";
1359 case dcp::VerificationNote::MISSING_FFOC:
1360 return "There should be a FFOC (first frame of content) marker";
1361 case dcp::VerificationNote::MISSING_LFOC:
1362 return "There should be a LFOC (last frame of content) marker";
1363 case dcp::VerificationNote::INCORRECT_FFOC:
1364 return "The FFOC marker should bet set to 1";
1365 case dcp::VerificationNote::INCORRECT_LFOC:
1366 return "The LFOC marker should be set to 1 less than the duration of the last reel";