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()) {
347 VerificationNote::Type::ERROR,
348 VerificationNote::Code::INVALID_XML,
350 boost::trim_copy(i.public_id() + " " + i.system_id()),
357 enum class VerifyAssetResult {
364 static VerifyAssetResult
365 verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelMXF> reel_mxf, function<void (float)> progress)
367 auto const actual_hash = reel_mxf->asset_ref()->hash(progress);
369 auto pkls = dcp->pkls();
370 /* We've read this DCP in so it must have at least one PKL */
371 DCP_ASSERT (!pkls.empty());
373 auto asset = reel_mxf->asset_ref().asset();
375 optional<string> pkl_hash;
377 pkl_hash = i->hash (reel_mxf->asset_ref()->id());
383 DCP_ASSERT (pkl_hash);
385 auto cpl_hash = reel_mxf->hash();
386 if (cpl_hash && *cpl_hash != *pkl_hash) {
387 return VerifyAssetResult::CPL_PKL_DIFFER;
390 if (actual_hash != *pkl_hash) {
391 return VerifyAssetResult::BAD;
394 return VerifyAssetResult::GOOD;
399 verify_language_tag (string tag, vector<VerificationNote>& notes)
402 LanguageTag test (tag);
403 } catch (LanguageTagError &) {
404 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, tag});
409 enum class VerifyPictureAssetResult
412 FRAME_NEARLY_TOO_LARGE,
418 biggest_frame_size (shared_ptr<const MonoPictureFrame> frame)
420 return frame->size ();
424 biggest_frame_size (shared_ptr<const StereoPictureFrame> frame)
426 return max(frame->left()->size(), frame->right()->size());
430 template <class A, class R, class F>
431 optional<VerifyPictureAssetResult>
432 verify_picture_asset_type (shared_ptr<const ReelMXF> reel_mxf, function<void (float)> progress)
434 auto asset = dynamic_pointer_cast<A>(reel_mxf->asset_ref().asset());
436 return optional<VerifyPictureAssetResult>();
439 int biggest_frame = 0;
440 auto reader = asset->start_read ();
441 auto const duration = asset->intrinsic_duration ();
442 for (int64_t i = 0; i < duration; ++i) {
443 shared_ptr<const F> frame = reader->get_frame (i);
444 biggest_frame = max(biggest_frame, biggest_frame_size(frame));
445 progress (float(i) / duration);
448 static const int max_frame = rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
449 static const int risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
450 if (biggest_frame > max_frame) {
451 return VerifyPictureAssetResult::BAD;
452 } else if (biggest_frame > risky_frame) {
453 return VerifyPictureAssetResult::FRAME_NEARLY_TOO_LARGE;
456 return VerifyPictureAssetResult::GOOD;
460 static VerifyPictureAssetResult
461 verify_picture_asset (shared_ptr<const ReelMXF> reel_mxf, function<void (float)> progress)
463 auto r = verify_picture_asset_type<MonoPictureAsset, MonoPictureAssetReader, MonoPictureFrame>(reel_mxf, progress);
465 r = verify_picture_asset_type<StereoPictureAsset, StereoPictureAssetReader, StereoPictureFrame>(reel_mxf, progress);
474 verify_main_picture_asset (
475 shared_ptr<const DCP> dcp,
476 shared_ptr<const ReelPictureAsset> reel_asset,
477 function<void (string, optional<boost::filesystem::path>)> stage,
478 function<void (float)> progress,
479 vector<VerificationNote>& notes
482 auto asset = reel_asset->asset();
483 auto const file = *asset->file();
484 stage ("Checking picture asset hash", file);
485 auto const r = verify_asset (dcp, reel_asset, progress);
487 case VerifyAssetResult::BAD:
489 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file
492 case VerifyAssetResult::CPL_PKL_DIFFER:
494 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file
500 stage ("Checking picture frame sizes", asset->file());
501 auto const pr = verify_picture_asset (reel_asset, progress);
503 case VerifyPictureAssetResult::BAD:
505 VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
508 case VerifyPictureAssetResult::FRAME_NEARLY_TOO_LARGE:
510 VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
517 /* Only flat/scope allowed by Bv2.1 */
519 asset->size() != Size(2048, 858) &&
520 asset->size() != Size(1998, 1080) &&
521 asset->size() != Size(4096, 1716) &&
522 asset->size() != Size(3996, 2160)) {
524 VerificationNote::Type::BV21_ERROR,
525 VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS,
526 String::compose("%1x%2", asset->size().width, asset->size().height),
531 /* Only 24, 25, 48fps allowed for 2K */
533 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
534 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
537 VerificationNote::Type::BV21_ERROR,
538 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
539 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
544 if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
545 /* Only 24fps allowed for 4K */
546 if (asset->edit_rate() != Fraction(24, 1)) {
548 VerificationNote::Type::BV21_ERROR,
549 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
550 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
555 /* Only 2D allowed for 4K */
556 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
558 VerificationNote::Type::BV21_ERROR,
559 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
560 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
571 verify_main_sound_asset (
572 shared_ptr<const DCP> dcp,
573 shared_ptr<const ReelSoundAsset> reel_asset,
574 function<void (string, optional<boost::filesystem::path>)> stage,
575 function<void (float)> progress,
576 vector<VerificationNote>& notes
579 auto asset = reel_asset->asset();
580 stage ("Checking sound asset hash", asset->file());
581 auto const r = verify_asset (dcp, reel_asset, progress);
583 case VerifyAssetResult::BAD:
584 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, *asset->file()});
586 case VerifyAssetResult::CPL_PKL_DIFFER:
587 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, *asset->file()});
593 stage ("Checking sound asset metadata", asset->file());
595 verify_language_tag (asset->language(), notes);
596 if (asset->sampling_rate() != 48000) {
597 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), *asset->file()});
603 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
605 /* XXX: is Language compulsory? */
606 if (reel_asset->language()) {
607 verify_language_tag (*reel_asset->language(), notes);
610 if (!reel_asset->entry_point()) {
611 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id() });
612 } else if (reel_asset->entry_point().get()) {
613 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
619 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
621 /* XXX: is Language compulsory? */
622 if (reel_asset->language()) {
623 verify_language_tag (*reel_asset->language(), notes);
626 if (!reel_asset->entry_point()) {
627 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
628 } else if (reel_asset->entry_point().get()) {
629 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
636 boost::optional<string> subtitle_language;
642 verify_smpte_subtitle_asset (
643 shared_ptr<const SMPTESubtitleAsset> asset,
644 vector<VerificationNote>& notes,
648 if (asset->language()) {
649 auto const language = *asset->language();
650 verify_language_tag (language, notes);
651 if (!state.subtitle_language) {
652 state.subtitle_language = language;
653 } else if (state.subtitle_language != language) {
654 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
657 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
659 auto const size = boost::filesystem::file_size(asset->file().get());
660 if (size > 115 * 1024 * 1024) {
662 { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file() }
665 /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
666 * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
668 auto fonts = asset->font_data ();
670 for (auto i: fonts) {
671 total_size += i.second.size();
673 if (total_size > 10 * 1024 * 1024) {
674 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
677 if (!asset->start_time()) {
678 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get() });
679 } else if (asset->start_time() != Time()) {
680 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() });
686 verify_subtitle_asset (
687 shared_ptr<const SubtitleAsset> asset,
688 function<void (string, optional<boost::filesystem::path>)> stage,
689 boost::filesystem::path xsd_dtd_directory,
690 vector<VerificationNote>& notes,
694 stage ("Checking subtitle XML", asset->file());
695 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
696 * gets passed through libdcp which may clean up and therefore hide errors.
698 validate_xml (asset->raw_xml(), xsd_dtd_directory, notes);
700 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
702 verify_smpte_subtitle_asset (smpte, notes, state);
708 verify_closed_caption_asset (
709 shared_ptr<const SubtitleAsset> asset,
710 function<void (string, optional<boost::filesystem::path>)> stage,
711 boost::filesystem::path xsd_dtd_directory,
712 vector<VerificationNote>& notes,
716 verify_subtitle_asset (asset, stage, xsd_dtd_directory, notes, state);
718 if (asset->raw_xml().size() > 256 * 1024) {
719 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(asset->raw_xml().size()), *asset->file()});
727 vector<shared_ptr<Reel>> reels,
728 optional<int> picture_frame_rate,
729 vector<VerificationNote>& notes,
730 std::function<bool (shared_ptr<Reel>)> check,
731 std::function<string (shared_ptr<Reel>)> xml,
732 std::function<int64_t (shared_ptr<Reel>)> duration
735 /* end of last subtitle (in editable units) */
736 optional<int64_t> last_out;
737 auto too_short = false;
738 auto too_close = false;
739 auto too_early = false;
740 /* current reel start time (in editable units) */
741 int64_t reel_offset = 0;
743 std::function<void (cxml::ConstNodePtr, int, int, bool)> parse;
744 parse = [&parse, &last_out, &too_short, &too_close, &too_early, &reel_offset](cxml::ConstNodePtr node, int tcr, int pfr, bool first_reel) {
745 if (node->name() == "Subtitle") {
746 Time in (node->string_attribute("TimeIn"), tcr);
747 Time out (node->string_attribute("TimeOut"), tcr);
748 if (first_reel && in < Time(0, 0, 4, 0, tcr)) {
751 auto length = out - in;
752 if (length.as_editable_units(pfr) < 15) {
756 /* XXX: this feels dubious - is it really what Bv2.1 means? */
757 auto distance = reel_offset + in.as_editable_units(pfr) - *last_out;
758 if (distance >= 0 && distance < 2) {
762 last_out = reel_offset + out.as_editable_units(pfr);
764 for (auto i: node->node_children()) {
765 parse(i, tcr, pfr, first_reel);
770 for (auto i = 0U; i < reels.size(); ++i) {
771 if (!check(reels[i])) {
775 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
776 * read in by libdcp's parser.
779 auto doc = make_shared<cxml::Document>("SubtitleReel");
780 doc->read_string (xml(reels[i]));
781 auto const tcr = doc->number_child<int>("TimeCodeRate");
782 parse (doc, tcr, picture_frame_rate.get_value_or(24), i == 0);
783 reel_offset += duration(reels[i]);
788 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
794 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
800 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
806 struct LinesCharactersResult
808 bool warning_length_exceeded = false;
809 bool error_length_exceeded = false;
810 bool line_count_exceeded = false;
816 verify_text_lines_and_characters (
817 shared_ptr<SubtitleAsset> asset,
820 LinesCharactersResult* result
826 Event (Time time_, float position_, int characters_)
828 , position (position_)
829 , characters (characters_)
832 Event (Time time_, shared_ptr<Event> start_)
838 int position; //< position from 0 at top of screen to 100 at bottom
840 shared_ptr<Event> start;
843 vector<shared_ptr<Event>> events;
845 auto position = [](shared_ptr<const SubtitleString> sub) {
846 switch (sub->v_align()) {
848 return lrintf(sub->v_position() * 100);
850 return lrintf((0.5f + sub->v_position()) * 100);
852 return lrintf((1.0f - sub->v_position()) * 100);
858 for (auto j: asset->subtitles()) {
859 auto text = dynamic_pointer_cast<const SubtitleString>(j);
861 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
862 events.push_back(in);
863 events.push_back(make_shared<Event>(text->out(), in));
867 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
868 return a->time < b->time;
871 map<int, int> current;
872 for (auto i: events) {
873 if (current.size() > 3) {
874 result->line_count_exceeded = true;
876 for (auto j: current) {
877 if (j.second >= warning_length) {
878 result->warning_length_exceeded = true;
880 if (j.second >= error_length) {
881 result->error_length_exceeded = true;
886 /* end of a subtitle */
887 DCP_ASSERT (current.find(i->start->position) != current.end());
888 if (current[i->start->position] == i->start->characters) {
889 current.erase(i->start->position);
891 current[i->start->position] -= i->start->characters;
894 /* start of a subtitle */
895 if (current.find(i->position) == current.end()) {
896 current[i->position] = i->characters;
898 current[i->position] += i->characters;
907 verify_text_timing (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
913 optional<int> picture_frame_rate;
914 if (reels[0]->main_picture()) {
915 picture_frame_rate = reels[0]->main_picture()->frame_rate().numerator;
918 if (reels[0]->main_subtitle()) {
919 verify_text_timing (reels, picture_frame_rate, notes,
920 [](shared_ptr<Reel> reel) {
921 return static_cast<bool>(reel->main_subtitle());
923 [](shared_ptr<Reel> reel) {
924 return reel->main_subtitle()->asset()->raw_xml();
926 [](shared_ptr<Reel> reel) {
927 return reel->main_subtitle()->actual_duration();
932 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
933 verify_text_timing (reels, picture_frame_rate, notes,
934 [i](shared_ptr<Reel> reel) {
935 return i < reel->closed_captions().size();
937 [i](shared_ptr<Reel> reel) {
938 return reel->closed_captions()[i]->asset()->raw_xml();
940 [i](shared_ptr<Reel> reel) {
941 return reel->closed_captions()[i]->actual_duration();
949 verify_extension_metadata (shared_ptr<CPL> cpl, vector<VerificationNote>& notes)
951 DCP_ASSERT (cpl->file());
952 cxml::Document doc ("CompositionPlaylist");
953 doc.read_file (cpl->file().get());
955 auto missing = false;
958 if (auto reel_list = doc.node_child("ReelList")) {
959 auto reels = reel_list->node_children("Reel");
960 if (!reels.empty()) {
961 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
962 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
963 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
965 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
966 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
970 if (auto name = extension->optional_node_child("Name")) {
971 if (name->content() != "Application") {
972 malformed = "<Name> should be 'Application'";
975 if (auto property_list = extension->optional_node_child("PropertyList")) {
976 if (auto property = property_list->optional_node_child("Property")) {
977 if (auto name = property->optional_node_child("Name")) {
978 if (name->content() != "DCP Constraints Profile") {
979 malformed = "<Name> property should be 'DCP Constraints Profile'";
982 if (auto value = property->optional_node_child("Value")) {
983 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
984 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
999 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1000 } else if (!malformed.empty()) {
1001 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1007 pkl_has_encrypted_assets (shared_ptr<DCP> dcp, shared_ptr<PKL> pkl)
1009 vector<string> encrypted;
1010 for (auto i: dcp->cpls()) {
1011 for (auto j: i->reel_mxfs()) {
1012 if (j->asset_ref().resolved()) {
1013 /* It's a bit surprising / broken but Interop subtitle assets are represented
1014 * in reels by ReelSubtitleAsset which inherits ReelMXF, so it's possible for
1015 * ReelMXFs to have assets which are not MXFs.
1017 if (auto asset = dynamic_pointer_cast<MXF>(j->asset_ref().asset())) {
1018 if (asset->encrypted()) {
1019 encrypted.push_back(j->asset_ref().id());
1026 for (auto i: pkl->asset_list()) {
1027 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1036 vector<VerificationNote>
1038 vector<boost::filesystem::path> directories,
1039 function<void (string, optional<boost::filesystem::path>)> stage,
1040 function<void (float)> progress,
1041 boost::filesystem::path xsd_dtd_directory
1044 xsd_dtd_directory = boost::filesystem::canonical (xsd_dtd_directory);
1046 vector<VerificationNote> notes;
1049 vector<shared_ptr<DCP>> dcps;
1050 for (auto i: directories) {
1051 dcps.push_back (shared_ptr<DCP> (new DCP (i)));
1054 for (auto dcp: dcps) {
1055 stage ("Checking DCP", dcp->directory());
1058 } catch (ReadError& e) {
1059 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1060 } catch (XMLError& e) {
1061 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1062 } catch (MXFFileError& e) {
1063 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1064 } catch (cxml::Error& e) {
1065 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1068 if (dcp->standard() != Standard::SMPTE) {
1069 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1072 for (auto cpl: dcp->cpls()) {
1073 stage ("Checking CPL", cpl->file());
1074 validate_xml (cpl->file().get(), xsd_dtd_directory, notes);
1076 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1077 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1080 for (auto const& i: cpl->additional_subtitle_languages()) {
1081 verify_language_tag (i, notes);
1084 if (cpl->release_territory()) {
1085 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") {
1086 auto terr = cpl->release_territory().get();
1087 /* Must be a valid region tag, or "001" */
1089 LanguageTag::RegionSubtag test (terr);
1091 if (terr != "001") {
1092 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1098 if (dcp->standard() == Standard::SMPTE) {
1099 if (!cpl->annotation_text()) {
1100 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1101 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1102 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1106 for (auto i: dcp->pkls()) {
1107 /* Check that the CPL's hash corresponds to the PKL */
1108 optional<string> h = i->hash(cpl->id());
1109 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1110 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1113 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1114 optional<string> required_annotation_text;
1115 for (auto j: i->asset_list()) {
1116 /* See if this is a CPL */
1117 for (auto k: dcp->cpls()) {
1118 if (j->id() == k->id()) {
1119 if (!required_annotation_text) {
1120 /* First CPL we have found; this is the required AnnotationText unless we find another */
1121 required_annotation_text = cpl->content_title_text();
1123 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1124 required_annotation_text = boost::none;
1130 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1131 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1135 /* set to true if any reel has a MainSubtitle */
1136 auto have_main_subtitle = false;
1137 /* set to true if any reel has no MainSubtitle */
1138 auto have_no_main_subtitle = false;
1139 /* fewest number of closed caption assets seen in a reel */
1140 size_t fewest_closed_captions = SIZE_MAX;
1141 /* most number of closed caption assets seen in a reel */
1142 size_t most_closed_captions = 0;
1143 map<Marker, Time> markers_seen;
1145 for (auto reel: cpl->reels()) {
1146 stage ("Checking reel", optional<boost::filesystem::path>());
1148 for (auto i: reel->assets()) {
1149 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1150 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1152 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1153 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1155 auto mxf = dynamic_pointer_cast<ReelMXF>(i);
1156 if (mxf && !mxf->hash()) {
1157 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1161 if (dcp->standard() == Standard::SMPTE) {
1162 boost::optional<int64_t> duration;
1163 for (auto i: reel->assets()) {
1165 duration = i->actual_duration();
1166 } else if (*duration != i->actual_duration()) {
1167 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1173 if (reel->main_picture()) {
1174 /* Check reel stuff */
1175 auto const frame_rate = reel->main_picture()->frame_rate();
1176 if (frame_rate.denominator != 1 ||
1177 (frame_rate.numerator != 24 &&
1178 frame_rate.numerator != 25 &&
1179 frame_rate.numerator != 30 &&
1180 frame_rate.numerator != 48 &&
1181 frame_rate.numerator != 50 &&
1182 frame_rate.numerator != 60 &&
1183 frame_rate.numerator != 96)) {
1185 VerificationNote::Type::ERROR,
1186 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1187 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1191 if (reel->main_picture()->asset_ref().resolved()) {
1192 verify_main_picture_asset (dcp, reel->main_picture(), stage, progress, notes);
1196 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1197 verify_main_sound_asset (dcp, reel->main_sound(), stage, progress, notes);
1200 if (reel->main_subtitle()) {
1201 verify_main_subtitle_reel (reel->main_subtitle(), notes);
1202 if (reel->main_subtitle()->asset_ref().resolved()) {
1203 verify_subtitle_asset (reel->main_subtitle()->asset(), stage, xsd_dtd_directory, notes, state);
1205 have_main_subtitle = true;
1207 have_no_main_subtitle = true;
1210 for (auto i: reel->closed_captions()) {
1211 verify_closed_caption_reel (i, notes);
1212 if (i->asset_ref().resolved()) {
1213 verify_closed_caption_asset (i->asset(), stage, xsd_dtd_directory, notes, state);
1217 if (reel->main_markers()) {
1218 for (auto const& i: reel->main_markers()->get()) {
1219 markers_seen.insert (i);
1223 fewest_closed_captions = std::min (fewest_closed_captions, reel->closed_captions().size());
1224 most_closed_captions = std::max (most_closed_captions, reel->closed_captions().size());
1227 if (dcp->standard() == Standard::SMPTE) {
1229 if (have_main_subtitle && have_no_main_subtitle) {
1230 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1233 if (fewest_closed_captions != most_closed_captions) {
1234 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1237 if (cpl->content_kind() == ContentKind::FEATURE) {
1238 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1239 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1241 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1242 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1246 auto ffoc = markers_seen.find(Marker::FFOC);
1247 if (ffoc == markers_seen.end()) {
1248 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1249 } else if (ffoc->second.e != 1) {
1250 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1253 auto lfoc = markers_seen.find(Marker::LFOC);
1254 if (lfoc == markers_seen.end()) {
1255 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1257 auto lfoc_time = lfoc->second.as_editable_units(lfoc->second.tcr);
1258 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1259 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1263 verify_text_timing (cpl->reels(), notes);
1265 LinesCharactersResult result;
1266 for (auto reel: cpl->reels()) {
1267 if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1268 verify_text_lines_and_characters (reel->main_subtitle()->asset(), 52, 79, &result);
1272 if (result.line_count_exceeded) {
1273 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1275 if (result.error_length_exceeded) {
1276 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1277 } else if (result.warning_length_exceeded) {
1278 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1281 result = LinesCharactersResult();
1282 for (auto reel: cpl->reels()) {
1283 for (auto i: reel->closed_captions()) {
1285 verify_text_lines_and_characters (i->asset(), 32, 32, &result);
1290 if (result.line_count_exceeded) {
1291 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1293 if (result.error_length_exceeded) {
1294 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1297 if (!cpl->full_content_title_text()) {
1298 /* Since FullContentTitleText is assumed always to exist if there's a CompositionMetadataAsset we
1299 * can use it as a proxy for CompositionMetadataAsset's existence.
1301 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1302 } else if (!cpl->version_number()) {
1303 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1306 verify_extension_metadata (cpl, notes);
1308 if (cpl->any_encrypted()) {
1309 cxml::Document doc ("CompositionPlaylist");
1310 DCP_ASSERT (cpl->file());
1311 doc.read_file (cpl->file().get());
1312 if (!doc.optional_node_child("Signature")) {
1313 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1319 for (auto pkl: dcp->pkls()) {
1320 stage ("Checking PKL", pkl->file());
1321 validate_xml (pkl->file().get(), xsd_dtd_directory, notes);
1322 if (pkl_has_encrypted_assets(dcp, pkl)) {
1323 cxml::Document doc ("PackingList");
1324 doc.read_file (pkl->file().get());
1325 if (!doc.optional_node_child("Signature")) {
1326 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1331 if (dcp->asset_map_path()) {
1332 stage ("Checking ASSETMAP", dcp->asset_map_path().get());
1333 validate_xml (dcp->asset_map_path().get(), xsd_dtd_directory, notes);
1335 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1343 dcp::note_to_string (VerificationNote note)
1345 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1347 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1348 * not "ClosedCaption assets must have an <EntryPoint> tag."
1350 * It's OK to use XML tag names where they are clear.
1351 * If both ID and filename are available, use only the ID.
1352 * End messages with a full stop.
1353 * Messages should not mention whether or not their errors are a part of Bv2.1.
1355 switch (note.code()) {
1356 case VerificationNote::Code::FAILED_READ:
1357 return *note.note();
1358 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1359 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1360 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1361 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1362 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1363 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1364 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1365 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1366 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1367 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1368 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1369 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1370 case VerificationNote::Code::EMPTY_ASSET_PATH:
1371 return "The asset map contains an empty asset path.";
1372 case VerificationNote::Code::MISSING_ASSET:
1373 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1374 case VerificationNote::Code::MISMATCHED_STANDARD:
1375 return "The DCP contains both SMPTE and Interop parts.";
1376 case VerificationNote::Code::INVALID_XML:
1377 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1378 case VerificationNote::Code::MISSING_ASSETMAP:
1379 return "No ASSETMAP or ASSETMAP.xml was found.";
1380 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1381 return String::compose("The intrinsic duration of the asset %1 is less than 1 second long.", note.note().get());
1382 case VerificationNote::Code::INVALID_DURATION:
1383 return String::compose("The duration of the asset %1 is less than 1 second long.", note.note().get());
1384 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1385 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());
1386 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1387 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());
1388 case VerificationNote::Code::EXTERNAL_ASSET:
1389 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());
1390 case VerificationNote::Code::INVALID_STANDARD:
1391 return "This DCP does not use the SMPTE standard.";
1392 case VerificationNote::Code::INVALID_LANGUAGE:
1393 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1394 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1395 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1396 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1397 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1398 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1399 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1400 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1401 return "3D 4K DCPs are not allowed.";
1402 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1403 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1404 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1405 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1406 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1407 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());
1408 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1409 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1410 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1411 return "Some subtitle assets have different <Language> tags than others";
1412 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1413 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1414 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1415 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1416 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1417 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1418 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1419 return "At least one subtitle lasts less than 15 frames.";
1420 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1421 return "At least one pair of subtitles is separated by less than 2 frames.";
1422 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1423 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1424 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1425 return "There are more than 52 characters in at least one subtitle line.";
1426 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1427 return "There are more than 79 characters in at least one subtitle line.";
1428 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1429 return "There are more than 3 closed caption lines in at least one place.";
1430 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1431 return "There are more than 32 characters in at least one closed caption line.";
1432 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1433 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1434 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1435 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1436 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1437 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>", note.note().get());
1438 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1439 return "All assets in a reel do not have the same duration.";
1440 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
1441 return "At least one reel contains a subtitle asset, but some reel(s) do not";
1442 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
1443 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1444 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
1445 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
1446 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
1447 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
1448 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
1449 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
1450 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
1451 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
1452 case VerificationNote::Code::MISSING_HASH:
1453 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
1454 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
1455 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker";
1456 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
1457 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker";
1458 case VerificationNote::Code::MISSING_FFOC:
1459 return "There should be a FFOC (first frame of content) marker";
1460 case VerificationNote::Code::MISSING_LFOC:
1461 return "There should be a LFOC (last frame of content) marker";
1462 case VerificationNote::Code::INCORRECT_FFOC:
1463 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
1464 case VerificationNote::Code::INCORRECT_LFOC:
1465 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
1466 case VerificationNote::Code::MISSING_CPL_METADATA:
1467 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
1468 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
1469 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
1470 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
1471 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
1472 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
1473 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
1474 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
1475 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
1476 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
1477 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
1478 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
1479 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>", note.note().get());
1480 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
1481 return "Some assets are encrypted but some are not";
1489 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
1491 return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
1495 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
1497 s << note_to_string (note);
1499 s << " [" << note.note().get() << "]";
1502 s << " [" << note.file().get() << "]";
1505 s << " [" << note.line().get() << "]";