X-Git-Url: https://main.carlh.net/gitweb/?a=blobdiff_plain;f=src%2Fverify.cc;h=0082cfcdbd8a22191194af58ce12f2a3a175eb5e;hb=14d1f9d76f289a6447a58f03813c771c86c7d8af;hp=1cea1a0c40ca0a6d1b387210acae5f52c5ba78dc;hpb=2915c4f48129a4cac2c8ca364b09dd8047364aad;p=libdcp.git diff --git a/src/verify.cc b/src/verify.cc index 1cea1a0c..0082cfcd 100644 --- a/src/verify.cc +++ b/src/verify.cc @@ -31,6 +31,12 @@ files in the program, then also delete it here. */ + +/** @file src/verify.cc + * @brief dcp::verify() method and associated code + */ + + #include "verify.h" #include "dcp.h" #include "cpl.h" @@ -67,12 +73,12 @@ #include #include #include -#include #include #include #include #include + using std::list; using std::vector; using std::string; @@ -86,9 +92,11 @@ using boost::optional; using boost::function; using std::dynamic_pointer_cast; + using namespace dcp; using namespace xercesc; + static string xml_ch_to_string (XMLCh const * a) @@ -99,6 +107,7 @@ xml_ch_to_string (XMLCh const * a) return o; } + class XMLValidationError { public: @@ -184,7 +193,8 @@ private: list _errors; }; -class StringToXMLCh : public boost::noncopyable + +class StringToXMLCh { public: StringToXMLCh (string a) @@ -192,6 +202,9 @@ public: _buffer = XMLString::transcode(a.c_str()); } + StringToXMLCh (StringToXMLCh const&) = delete; + StringToXMLCh& operator= (StringToXMLCh const&) = delete; + ~StringToXMLCh () { XMLString::release (&_buffer); @@ -205,6 +218,7 @@ private: XMLCh* _buffer; }; + class LocalFileResolver : public EntityResolver { public: @@ -343,40 +357,38 @@ validate_xml (T xml, boost::filesystem::path xsd_dtd_directory, vector dcp, shared_ptr reel_mxf, function progress) +verify_asset (shared_ptr dcp, shared_ptr reel_file_asset, function progress) { - auto const actual_hash = reel_mxf->asset_ref()->hash(progress); + auto const actual_hash = reel_file_asset->asset_ref()->hash(progress); auto pkls = dcp->pkls(); /* We've read this DCP in so it must have at least one PKL */ DCP_ASSERT (!pkls.empty()); - auto asset = reel_mxf->asset_ref().asset(); + auto asset = reel_file_asset->asset_ref().asset(); optional pkl_hash; for (auto i: pkls) { - pkl_hash = i->hash (reel_mxf->asset_ref()->id()); + pkl_hash = i->hash (reel_file_asset->asset_ref()->id()); if (pkl_hash) { break; } @@ -384,16 +396,16 @@ verify_asset (shared_ptr dcp, shared_ptr reel_mxf, fun DCP_ASSERT (pkl_hash); - auto cpl_hash = reel_mxf->hash(); + auto cpl_hash = reel_file_asset->hash(); if (cpl_hash && *cpl_hash != *pkl_hash) { - return VERIFY_ASSET_RESULT_CPL_PKL_DIFFER; + return VerifyAssetResult::CPL_PKL_DIFFER; } if (actual_hash != *pkl_hash) { - return VERIFY_ASSET_RESULT_BAD; + return VerifyAssetResult::BAD; } - return VERIFY_ASSET_RESULT_GOOD; + return VerifyAssetResult::GOOD; } @@ -401,18 +413,18 @@ void verify_language_tag (string tag, vector& notes) { try { - dcp::LanguageTag test (tag); - } catch (dcp::LanguageTagError &) { - notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::BAD_LANGUAGE, tag)); + LanguageTag test (tag); + } catch (LanguageTagError &) { + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, tag}); } } -enum VerifyPictureAssetResult +enum class VerifyPictureAssetResult { - VERIFY_PICTURE_ASSET_RESULT_GOOD, - VERIFY_PICTURE_ASSET_RESULT_FRAME_NEARLY_TOO_LARGE, - VERIFY_PICTURE_ASSET_RESULT_BAD, + GOOD, + FRAME_NEARLY_TOO_LARGE, + BAD, }; @@ -431,9 +443,9 @@ biggest_frame_size (shared_ptr frame) template optional -verify_picture_asset_type (shared_ptr reel_mxf, function progress) +verify_picture_asset_type (shared_ptr reel_file_asset, function progress) { - auto asset = dynamic_pointer_cast(reel_mxf->asset_ref().asset()); + auto asset = dynamic_pointer_cast(reel_file_asset->asset_ref().asset()); if (!asset) { return optional(); } @@ -450,21 +462,21 @@ verify_picture_asset_type (shared_ptr reel_mxf, functionedit_rate().as_float())); static const int risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float())); if (biggest_frame > max_frame) { - return VERIFY_PICTURE_ASSET_RESULT_BAD; + return VerifyPictureAssetResult::BAD; } else if (biggest_frame > risky_frame) { - return VERIFY_PICTURE_ASSET_RESULT_FRAME_NEARLY_TOO_LARGE; + return VerifyPictureAssetResult::FRAME_NEARLY_TOO_LARGE; } - return VERIFY_PICTURE_ASSET_RESULT_GOOD; + return VerifyPictureAssetResult::GOOD; } static VerifyPictureAssetResult -verify_picture_asset (shared_ptr reel_mxf, function progress) +verify_picture_asset (shared_ptr reel_file_asset, function progress) { - auto r = verify_picture_asset_type(reel_mxf, progress); + auto r = verify_picture_asset_type(reel_file_asset, progress); if (!r) { - r = verify_picture_asset_type(reel_mxf, progress); + r = verify_picture_asset_type(reel_file_asset, progress); } DCP_ASSERT (r); @@ -486,19 +498,15 @@ verify_main_picture_asset ( stage ("Checking picture asset hash", file); auto const r = verify_asset (dcp, reel_asset, progress); switch (r) { - case VERIFY_ASSET_RESULT_BAD: - notes.push_back ( - VerificationNote( - VerificationNote::VERIFY_ERROR, VerificationNote::PICTURE_HASH_INCORRECT, file - ) - ); + case VerifyAssetResult::BAD: + notes.push_back ({ + VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file + }); break; - case VERIFY_ASSET_RESULT_CPL_PKL_DIFFER: - notes.push_back ( - VerificationNote( - VerificationNote::VERIFY_ERROR, VerificationNote::PKL_CPL_PICTURE_HASHES_DIFFER, file - ) - ); + case VerifyAssetResult::CPL_PKL_DIFFER: + notes.push_back ({ + VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file + }); break; default: break; @@ -506,19 +514,15 @@ verify_main_picture_asset ( stage ("Checking picture frame sizes", asset->file()); auto const pr = verify_picture_asset (reel_asset, progress); switch (pr) { - case VERIFY_PICTURE_ASSET_RESULT_BAD: - notes.push_back ( - VerificationNote( - VerificationNote::VERIFY_ERROR, VerificationNote::PICTURE_FRAME_TOO_LARGE_IN_BYTES, file - ) - ); + case VerifyPictureAssetResult::BAD: + notes.push_back ({ + VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file + }); break; - case VERIFY_PICTURE_ASSET_RESULT_FRAME_NEARLY_TOO_LARGE: - notes.push_back ( - VerificationNote( - VerificationNote::VERIFY_WARNING, VerificationNote::PICTURE_FRAME_NEARLY_TOO_LARGE_IN_BYTES, file - ) - ); + case VerifyPictureAssetResult::FRAME_NEARLY_TOO_LARGE: + notes.push_back ({ + VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file + }); break; default: break; @@ -526,57 +530,50 @@ verify_main_picture_asset ( /* Only flat/scope allowed by Bv2.1 */ if ( - asset->size() != dcp::Size(2048, 858) && - asset->size() != dcp::Size(1998, 1080) && - asset->size() != dcp::Size(4096, 1716) && - asset->size() != dcp::Size(3996, 2160)) { - notes.push_back( - VerificationNote( - VerificationNote::VERIFY_BV21_ERROR, - VerificationNote::PICTURE_ASSET_INVALID_SIZE_IN_PIXELS, - String::compose("%1x%2", asset->size().width, asset->size().height), - file - ) - ); + asset->size() != Size(2048, 858) && + asset->size() != Size(1998, 1080) && + asset->size() != Size(4096, 1716) && + asset->size() != Size(3996, 2160)) { + notes.push_back({ + VerificationNote::Type::BV21_ERROR, + VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS, + String::compose("%1x%2", asset->size().width, asset->size().height), + file + }); } /* Only 24, 25, 48fps allowed for 2K */ if ( - (asset->size() == dcp::Size(2048, 858) || asset->size() == dcp::Size(1998, 1080)) && - (asset->edit_rate() != dcp::Fraction(24, 1) && asset->edit_rate() != dcp::Fraction(25, 1) && asset->edit_rate() != dcp::Fraction(48, 1)) + (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) && + (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1)) ) { - notes.push_back( - VerificationNote( - VerificationNote::VERIFY_BV21_ERROR, - VerificationNote::PICTURE_ASSET_INVALID_FRAME_RATE_FOR_2K, - String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator), - file - ) - ); + notes.push_back({ + VerificationNote::Type::BV21_ERROR, + VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K, + String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator), + file + }); } - if (asset->size() == dcp::Size(4096, 1716) || asset->size() == dcp::Size(3996, 2160)) { + if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) { /* Only 24fps allowed for 4K */ - if (asset->edit_rate() != dcp::Fraction(24, 1)) { - notes.push_back( - VerificationNote( - VerificationNote::VERIFY_BV21_ERROR, - VerificationNote::PICTURE_ASSET_INVALID_FRAME_RATE_FOR_4K, - String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator), - file - ) - ); + if (asset->edit_rate() != Fraction(24, 1)) { + notes.push_back({ + VerificationNote::Type::BV21_ERROR, + VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K, + String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator), + file + }); } /* Only 2D allowed for 4K */ if (dynamic_pointer_cast(asset)) { - notes.push_back( - VerificationNote( - VerificationNote::VERIFY_BV21_ERROR, - VerificationNote::PICTURE_ASSET_4K_3D, - file - ) - ); + notes.push_back({ + VerificationNote::Type::BV21_ERROR, + VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D, + String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator), + file + }); } } @@ -597,19 +594,11 @@ verify_main_sound_asset ( stage ("Checking sound asset hash", asset->file()); auto const r = verify_asset (dcp, reel_asset, progress); switch (r) { - case VERIFY_ASSET_RESULT_BAD: - notes.push_back ( - VerificationNote( - VerificationNote::VERIFY_ERROR, VerificationNote::SOUND_HASH_INCORRECT, *asset->file() - ) - ); + case VerifyAssetResult::BAD: + notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, *asset->file()}); break; - case VERIFY_ASSET_RESULT_CPL_PKL_DIFFER: - notes.push_back ( - VerificationNote( - VerificationNote::VERIFY_ERROR, VerificationNote::PKL_CPL_SOUND_HASHES_DIFFER, *asset->file() - ) - ); + case VerifyAssetResult::CPL_PKL_DIFFER: + notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, *asset->file()}); break; default: break; @@ -619,11 +608,7 @@ verify_main_sound_asset ( verify_language_tag (asset->language(), notes); if (asset->sampling_rate() != 48000) { - notes.push_back ( - VerificationNote( - VerificationNote::VERIFY_BV21_ERROR, VerificationNote::INVALID_SOUND_FRAME_RATE, *asset->file() - ) - ); + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert(asset->sampling_rate()), *asset->file()}); } } @@ -637,9 +622,9 @@ verify_main_subtitle_reel (shared_ptr reel_asset, vecto } if (!reel_asset->entry_point()) { - notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_SUBTITLE_ENTRY_POINT }); + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id() }); } else if (reel_asset->entry_point().get()) { - notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::SUBTITLE_ENTRY_POINT_NON_ZERO }); + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() }); } } @@ -653,9 +638,9 @@ verify_closed_caption_reel (shared_ptr reel_asset, } if (!reel_asset->entry_point()) { - notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_CLOSED_CAPTION_ENTRY_POINT }); + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() }); } else if (reel_asset->entry_point().get()) { - notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::CLOSED_CAPTION_ENTRY_POINT_NON_ZERO }); + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() }); } } @@ -669,7 +654,7 @@ struct State void verify_smpte_subtitle_asset ( - shared_ptr asset, + shared_ptr asset, vector& notes, State& state ) @@ -680,24 +665,15 @@ verify_smpte_subtitle_asset ( if (!state.subtitle_language) { state.subtitle_language = language; } else if (state.subtitle_language != language) { - notes.push_back ( - VerificationNote( - VerificationNote::VERIFY_BV21_ERROR, VerificationNote::SUBTITLE_LANGUAGES_DIFFER, *asset->file() - ) - ); + notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES }); } } else { - notes.push_back ( - VerificationNote( - VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_SUBTITLE_LANGUAGE, *asset->file() - ) - ); + notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() }); } - if (boost::filesystem::file_size(*asset->file()) > 115 * 1024 * 1024) { + auto const size = boost::filesystem::file_size(asset->file().get()); + if (size > 115 * 1024 * 1024) { notes.push_back ( - VerificationNote( - VerificationNote::VERIFY_BV21_ERROR, VerificationNote::TIMED_TEXT_ASSET_TOO_LARGE_IN_BYTES, *asset->file() - ) + { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert(size), *asset->file() } ); } /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB" @@ -709,23 +685,13 @@ verify_smpte_subtitle_asset ( total_size += i.second.size(); } if (total_size > 10 * 1024 * 1024) { - notes.push_back ( - VerificationNote( - VerificationNote::VERIFY_BV21_ERROR, VerificationNote::TIMED_TEXT_FONTS_TOO_LARGE_IN_BYTES, *asset->file() - ) - ); + notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert(total_size), asset->file().get() }); } if (!asset->start_time()) { - notes.push_back ( - VerificationNote( - VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_SUBTITLE_START_TIME, *asset->file()) - ); - } else if (asset->start_time() != dcp::Time()) { - notes.push_back ( - VerificationNote( - VerificationNote::VERIFY_BV21_ERROR, VerificationNote::SUBTITLE_START_TIME_NON_ZERO, *asset->file()) - ); + notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get() }); + } else if (asset->start_time() != Time()) { + notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() }); } } @@ -764,24 +730,20 @@ verify_closed_caption_asset ( verify_subtitle_asset (asset, stage, xsd_dtd_directory, notes, state); if (asset->raw_xml().size() > 256 * 1024) { - notes.push_back ( - VerificationNote( - VerificationNote::VERIFY_BV21_ERROR, VerificationNote::CLOSED_CAPTION_XML_TOO_LARGE_IN_BYTES, *asset->file() - ) - ); + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert(asset->raw_xml().size()), *asset->file()}); } } static void -check_text_timing ( - vector> reels, +verify_text_timing ( + vector> reels, optional picture_frame_rate, vector& notes, - std::function)> check, - std::function)> xml, - std::function)> duration + std::function)> check, + std::function)> xml, + std::function)> duration ) { /* end of last subtitle (in editable units) */ @@ -795,9 +757,9 @@ check_text_timing ( std::function parse; parse = [&parse, &last_out, &too_short, &too_close, &too_early, &reel_offset](cxml::ConstNodePtr node, int tcr, int pfr, bool first_reel) { if (node->name() == "Subtitle") { - dcp::Time in (node->string_attribute("TimeIn"), tcr); - dcp::Time out (node->string_attribute("TimeOut"), tcr); - if (first_reel && in < dcp::Time(0, 0, 4, 0, tcr)) { + Time in (node->string_attribute("TimeIn"), tcr); + Time out (node->string_attribute("TimeOut"), tcr); + if (first_reel && in < Time(0, 0, 4, 0, tcr)) { too_early = true; } auto length = out - in; @@ -836,27 +798,21 @@ check_text_timing ( } if (too_early) { - notes.push_back( - VerificationNote( - VerificationNote::VERIFY_WARNING, VerificationNote::FIRST_TEXT_TOO_EARLY - ) - ); + notes.push_back({ + VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME + }); } if (too_short) { - notes.push_back ( - VerificationNote( - VerificationNote::VERIFY_WARNING, VerificationNote::SUBTITLE_TOO_SHORT - ) - ); + notes.push_back ({ + VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION + }); } if (too_close) { - notes.push_back ( - VerificationNote( - VerificationNote::VERIFY_WARNING, VerificationNote::SUBTITLE_TOO_CLOSE - ) - ); + notes.push_back ({ + VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING + }); } } @@ -871,7 +827,7 @@ struct LinesCharactersResult static void -check_text_lines_and_characters ( +verify_text_lines_and_characters ( shared_ptr asset, int warning_length, int error_length, @@ -881,18 +837,18 @@ check_text_lines_and_characters ( class Event { public: - Event (dcp::Time time_, float position_, int characters_) + Event (Time time_, float position_, int characters_) : time (time_) , position (position_) , characters (characters_) {} - Event (dcp::Time time_, shared_ptr start_) + Event (Time time_, shared_ptr start_) : time (time_) , start (start_) {} - dcp::Time time; + Time time; int position; //< position from 0 at top of screen to 100 at bottom int characters; shared_ptr start; @@ -902,11 +858,11 @@ check_text_lines_and_characters ( auto position = [](shared_ptr sub) { switch (sub->v_align()) { - case VALIGN_TOP: + case VAlign::TOP: return lrintf(sub->v_position() * 100); - case VALIGN_CENTER: + case VAlign::CENTER: return lrintf((0.5f + sub->v_position()) * 100); - case VALIGN_BOTTOM: + case VAlign::BOTTOM: return lrintf((1.0f - sub->v_position()) * 100); } @@ -962,7 +918,7 @@ check_text_lines_and_characters ( static void -check_text_timing (vector> reels, vector& notes) +verify_text_timing (vector> reels, vector& notes) { if (reels.empty()) { return; @@ -974,28 +930,28 @@ check_text_timing (vector> reels, vector } if (reels[0]->main_subtitle()) { - check_text_timing (reels, picture_frame_rate, notes, - [](shared_ptr reel) { + verify_text_timing (reels, picture_frame_rate, notes, + [](shared_ptr reel) { return static_cast(reel->main_subtitle()); }, - [](shared_ptr reel) { + [](shared_ptr reel) { return reel->main_subtitle()->asset()->raw_xml(); }, - [](shared_ptr reel) { + [](shared_ptr reel) { return reel->main_subtitle()->actual_duration(); } ); } for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) { - check_text_timing (reels, picture_frame_rate, notes, - [i](shared_ptr reel) { + verify_text_timing (reels, picture_frame_rate, notes, + [i](shared_ptr reel) { return i < reel->closed_captions().size(); }, - [i](shared_ptr reel) { + [i](shared_ptr reel) { return reel->closed_captions()[i]->asset()->raw_xml(); }, - [i](shared_ptr reel) { + [i](shared_ptr reel) { return reel->closed_captions()[i]->actual_duration(); } ); @@ -1003,6 +959,94 @@ check_text_timing (vector> reels, vector } +void +verify_extension_metadata (shared_ptr cpl, vector& notes) +{ + DCP_ASSERT (cpl->file()); + cxml::Document doc ("CompositionPlaylist"); + doc.read_file (cpl->file().get()); + + auto missing = false; + string malformed; + + if (auto reel_list = doc.node_child("ReelList")) { + auto reels = reel_list->node_children("Reel"); + if (!reels.empty()) { + if (auto asset_list = reels[0]->optional_node_child("AssetList")) { + if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) { + if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) { + missing = true; + for (auto extension: extension_list->node_children("ExtensionMetadata")) { + if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") { + continue; + } + missing = false; + if (auto name = extension->optional_node_child("Name")) { + if (name->content() != "Application") { + malformed = " should be 'Application'"; + } + } + if (auto property_list = extension->optional_node_child("PropertyList")) { + if (auto property = property_list->optional_node_child("Property")) { + if (auto name = property->optional_node_child("Name")) { + if (name->content() != "DCP Constraints Profile") { + malformed = " property should be 'DCP Constraints Profile'"; + } + } + if (auto value = property->optional_node_child("Value")) { + if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") { + malformed = " property should be 'SMPTE-RDD-52:2020-Bv2.1'"; + } + } + } + } + } + } else { + missing = true; + } + } + } + } + } + + if (missing) { + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()}); + } else if (!malformed.empty()) { + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()}); + } +} + + +bool +pkl_has_encrypted_assets (shared_ptr dcp, shared_ptr pkl) +{ + vector encrypted; + for (auto i: dcp->cpls()) { + for (auto j: i->reel_file_assets()) { + if (j->asset_ref().resolved()) { + /* It's a bit surprising / broken but Interop subtitle assets are represented + * in reels by ReelSubtitleAsset which inherits ReelFileAsset, so it's possible for + * ReelFileAssets to have assets which are not MXFs. + */ + if (auto asset = dynamic_pointer_cast(j->asset_ref().asset())) { + if (asset->encrypted()) { + encrypted.push_back(j->asset_ref().id()); + } + } + } + } + } + + for (auto i: pkl->asset_list()) { + if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) { + return true; + } + } + + return false; +} + + vector dcp::verify ( vector directories, @@ -1014,7 +1058,7 @@ dcp::verify ( xsd_dtd_directory = boost::filesystem::canonical (xsd_dtd_directory); vector notes; - State state; + State state{}; vector> dcps; for (auto i: directories) { @@ -1026,23 +1070,27 @@ dcp::verify ( try { dcp->read (¬es); } catch (ReadError& e) { - notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::GENERAL_READ, string(e.what()))); + notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())}); } catch (XMLError& e) { - notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::GENERAL_READ, string(e.what()))); + notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())}); } catch (MXFFileError& e) { - notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::GENERAL_READ, string(e.what()))); + notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())}); } catch (cxml::Error& e) { - notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::GENERAL_READ, string(e.what()))); + notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())}); } - if (dcp->standard() != dcp::SMPTE) { - notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::NOT_SMPTE)); + if (dcp->standard() != Standard::SMPTE) { + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD}); } for (auto cpl: dcp->cpls()) { stage ("Checking CPL", cpl->file()); validate_xml (cpl->file().get(), xsd_dtd_directory, notes); + if (cpl->any_encrypted() && !cpl->all_encrypted()) { + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED}); + } + for (auto const& i: cpl->additional_subtitle_languages()) { verify_language_tag (i, notes); } @@ -1055,25 +1103,46 @@ dcp::verify ( LanguageTag::RegionSubtag test (terr); } catch (...) { if (terr != "001") { - notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::BAD_LANGUAGE, terr}); + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr}); } } } } - if (dcp->standard() == dcp::SMPTE) { + if (dcp->standard() == Standard::SMPTE) { if (!cpl->annotation_text()) { - notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_ANNOTATION_TEXT_IN_CPL)); + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()}); } else if (cpl->annotation_text().get() != cpl->content_title_text()) { - notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::CPL_ANNOTATION_TEXT_DIFFERS_FROM_CONTENT_TITLE_TEXT)); + notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()}); } } - /* Check that the CPL's hash corresponds to the PKL */ for (auto i: dcp->pkls()) { + /* Check that the CPL's hash corresponds to the PKL */ optional h = i->hash(cpl->id()); if (h && make_digest(ArrayData(*cpl->file())) != *h) { - notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::CPL_HASH_INCORRECT)); + notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()}); + } + + /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */ + optional required_annotation_text; + for (auto j: i->asset_list()) { + /* See if this is a CPL */ + for (auto k: dcp->cpls()) { + if (j->id() == k->id()) { + if (!required_annotation_text) { + /* First CPL we have found; this is the required AnnotationText unless we find another */ + required_annotation_text = cpl->content_title_text(); + } else { + /* There's more than one CPL so we don't care what the PKL's AnnotationText is */ + required_annotation_text = boost::none; + } + } + } + } + + if (required_annotation_text && i->annotation_text() != required_annotation_text) { + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()}); } } @@ -1092,24 +1161,24 @@ dcp::verify ( for (auto i: reel->assets()) { if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) { - notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::DURATION_TOO_SMALL, i->id())); + notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()}); } if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) { - notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::INTRINSIC_DURATION_TOO_SMALL, i->id())); + notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()}); } - auto mxf = dynamic_pointer_cast(i); - if (mxf && !mxf->hash()) { - notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_HASH, i->id()}); + auto file_asset = dynamic_pointer_cast(i); + if (file_asset && !file_asset->hash()) { + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()}); } } - if (dcp->standard() == dcp::SMPTE) { + if (dcp->standard() == Standard::SMPTE) { boost::optional duration; for (auto i: reel->assets()) { if (!duration) { duration = i->actual_duration(); } else if (*duration != i->actual_duration()) { - notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISMATCHED_ASSET_DURATION, i->id())); + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION}); break; } } @@ -1126,7 +1195,11 @@ dcp::verify ( frame_rate.numerator != 50 && frame_rate.numerator != 60 && frame_rate.numerator != 96)) { - notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::INVALID_PICTURE_FRAME_RATE)); + notes.push_back ({ + VerificationNote::Type::ERROR, + VerificationNote::Code::INVALID_PICTURE_FRAME_RATE, + String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator) + }); } /* Check asset */ if (reel->main_picture()->asset_ref().resolved()) { @@ -1165,71 +1238,94 @@ dcp::verify ( most_closed_captions = std::max (most_closed_captions, reel->closed_captions().size()); } - if (dcp->standard() == dcp::SMPTE) { + if (dcp->standard() == Standard::SMPTE) { if (have_main_subtitle && have_no_main_subtitle) { - notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MAIN_SUBTITLE_NOT_IN_ALL_REELS}); + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS}); } if (fewest_closed_captions != most_closed_captions) { - notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::CLOSED_CAPTION_ASSET_COUNTS_DIFFER}); + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS}); } - if (cpl->content_kind() == FEATURE) { - if (markers_seen.find(dcp::Marker::FFEC) == markers_seen.end()) { - notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_FFEC_IN_FEATURE}); + if (cpl->content_kind() == ContentKind::FEATURE) { + if (markers_seen.find(Marker::FFEC) == markers_seen.end()) { + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE}); } - if (markers_seen.find(dcp::Marker::FFMC) == markers_seen.end()) { - notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_FFMC_IN_FEATURE}); + if (markers_seen.find(Marker::FFMC) == markers_seen.end()) { + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE}); } } - auto ffoc = markers_seen.find(dcp::Marker::FFOC); + auto ffoc = markers_seen.find(Marker::FFOC); if (ffoc == markers_seen.end()) { - notes.push_back ({VerificationNote::VERIFY_WARNING, VerificationNote::MISSING_FFOC}); + notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC}); } else if (ffoc->second.e != 1) { - notes.push_back ({VerificationNote::VERIFY_WARNING, VerificationNote::INCORRECT_FFOC}); + notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert(ffoc->second.e)}); } - auto lfoc = markers_seen.find(dcp::Marker::LFOC); + auto lfoc = markers_seen.find(Marker::LFOC); if (lfoc == markers_seen.end()) { - notes.push_back ({VerificationNote::VERIFY_WARNING, VerificationNote::MISSING_LFOC}); - } else if (lfoc->second.as_editable_units(lfoc->second.tcr) != (cpl->reels().back()->duration() - 1)) { - notes.push_back ({VerificationNote::VERIFY_WARNING, VerificationNote::INCORRECT_LFOC}); + notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC}); + } else { + auto lfoc_time = lfoc->second.as_editable_units(lfoc->second.tcr); + if (lfoc_time != (cpl->reels().back()->duration() - 1)) { + notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert(lfoc_time)}); + } } - check_text_timing (cpl->reels(), notes); + verify_text_timing (cpl->reels(), notes); LinesCharactersResult result; for (auto reel: cpl->reels()) { if (reel->main_subtitle() && reel->main_subtitle()->asset()) { - check_text_lines_and_characters (reel->main_subtitle()->asset(), 52, 79, &result); + verify_text_lines_and_characters (reel->main_subtitle()->asset(), 52, 79, &result); } } if (result.line_count_exceeded) { - notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::TOO_MANY_SUBTITLE_LINES)); + notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT}); } if (result.error_length_exceeded) { - notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::SUBTITLE_LINE_TOO_LONG)); + notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH}); } else if (result.warning_length_exceeded) { - notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::SUBTITLE_LINE_LONGER_THAN_RECOMMENDED)); + notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH}); } result = LinesCharactersResult(); for (auto reel: cpl->reels()) { for (auto i: reel->closed_captions()) { if (i->asset()) { - check_text_lines_and_characters (i->asset(), 32, 32, &result); + verify_text_lines_and_characters (i->asset(), 32, 32, &result); } } } if (result.line_count_exceeded) { - notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::TOO_MANY_CLOSED_CAPTION_LINES)); + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT}); } if (result.error_length_exceeded) { - notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::CLOSED_CAPTION_LINE_TOO_LONG)); + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH}); + } + + if (!cpl->full_content_title_text()) { + /* Since FullContentTitleText is assumed always to exist if there's a CompositionMetadataAsset we + * can use it as a proxy for CompositionMetadataAsset's existence. + */ + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()}); + } else if (!cpl->version_number()) { + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()}); + } + + verify_extension_metadata (cpl, notes); + + if (cpl->any_encrypted()) { + cxml::Document doc ("CompositionPlaylist"); + DCP_ASSERT (cpl->file()); + doc.read_file (cpl->file().get()); + if (!doc.optional_node_child("Signature")) { + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()}); + } } } } @@ -1237,134 +1333,219 @@ dcp::verify ( for (auto pkl: dcp->pkls()) { stage ("Checking PKL", pkl->file()); validate_xml (pkl->file().get(), xsd_dtd_directory, notes); + if (pkl_has_encrypted_assets(dcp, pkl)) { + cxml::Document doc ("PackingList"); + doc.read_file (pkl->file().get()); + if (!doc.optional_node_child("Signature")) { + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()}); + } + } } if (dcp->asset_map_path()) { stage ("Checking ASSETMAP", dcp->asset_map_path().get()); validate_xml (dcp->asset_map_path().get(), xsd_dtd_directory, notes); } else { - notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::MISSING_ASSETMAP)); + notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP}); } } return notes; } + string -dcp::note_to_string (dcp::VerificationNote note) +dcp::note_to_string (VerificationNote note) { + /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.). + * + * e.g. "ClosedCaption asset has no tag.", + * not "ClosedCaption assets must have an tag." + * + * It's OK to use XML tag names where they are clear. + * If both ID and filename are available, use only the ID. + * End messages with a full stop. + * Messages should not mention whether or not their errors are a part of Bv2.1. + */ switch (note.code()) { - case dcp::VerificationNote::GENERAL_READ: + case VerificationNote::Code::FAILED_READ: return *note.note(); - case dcp::VerificationNote::CPL_HASH_INCORRECT: - return "The hash of the CPL in the PKL does not agree with the CPL file."; - case dcp::VerificationNote::INVALID_PICTURE_FRAME_RATE: - return "The picture in a reel has an invalid frame rate."; - case dcp::VerificationNote::PICTURE_HASH_INCORRECT: - return dcp::String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename()); - case dcp::VerificationNote::PKL_CPL_PICTURE_HASHES_DIFFER: - return dcp::String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename()); - case dcp::VerificationNote::SOUND_HASH_INCORRECT: - return dcp::String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename()); - case dcp::VerificationNote::PKL_CPL_SOUND_HASHES_DIFFER: - return dcp::String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename()); - case dcp::VerificationNote::EMPTY_ASSET_PATH: + case VerificationNote::Code::MISMATCHED_CPL_HASHES: + return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get()); + case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE: + return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get()); + case VerificationNote::Code::INCORRECT_PICTURE_HASH: + return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename()); + case VerificationNote::Code::MISMATCHED_PICTURE_HASHES: + return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename()); + case VerificationNote::Code::INCORRECT_SOUND_HASH: + return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename()); + case VerificationNote::Code::MISMATCHED_SOUND_HASHES: + return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename()); + case VerificationNote::Code::EMPTY_ASSET_PATH: return "The asset map contains an empty asset path."; - case dcp::VerificationNote::MISSING_ASSET: - return String::compose("The file for an asset in the asset map cannot be found; missing file is %1.", note.file()->filename()); - case dcp::VerificationNote::MISMATCHED_STANDARD: + case VerificationNote::Code::MISSING_ASSET: + return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename()); + case VerificationNote::Code::MISMATCHED_STANDARD: return "The DCP contains both SMPTE and Interop parts."; - case dcp::VerificationNote::XML_VALIDATION_ERROR: + case VerificationNote::Code::INVALID_XML: return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get()); - case dcp::VerificationNote::MISSING_ASSETMAP: + case VerificationNote::Code::MISSING_ASSETMAP: return "No ASSETMAP or ASSETMAP.xml was found."; - case dcp::VerificationNote::INTRINSIC_DURATION_TOO_SMALL: - return String::compose("The intrinsic duration of an asset is less than 1 second long: %1", note.note().get()); - case dcp::VerificationNote::DURATION_TOO_SMALL: - return String::compose("The duration of an asset is less than 1 second long: %1", note.note().get()); - case dcp::VerificationNote::PICTURE_FRAME_TOO_LARGE_IN_BYTES: + case VerificationNote::Code::INVALID_INTRINSIC_DURATION: + return String::compose("The intrinsic duration of the asset %1 is less than 1 second long.", note.note().get()); + case VerificationNote::Code::INVALID_DURATION: + return String::compose("The duration of the asset %1 is less than 1 second long.", note.note().get()); + case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES: 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()); - case dcp::VerificationNote::PICTURE_FRAME_NEARLY_TOO_LARGE_IN_BYTES: + case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES: 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()); - case dcp::VerificationNote::EXTERNAL_ASSET: - 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()); - case dcp::VerificationNote::NOT_SMPTE: - return "This DCP does not use the SMPTE standard, which is required for Bv2.1 compliance."; - case dcp::VerificationNote::BAD_LANGUAGE: + case VerificationNote::Code::EXTERNAL_ASSET: + 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()); + case VerificationNote::Code::INVALID_STANDARD: + return "This DCP does not use the SMPTE standard."; + case VerificationNote::Code::INVALID_LANGUAGE: return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get()); - case dcp::VerificationNote::PICTURE_ASSET_INVALID_SIZE_IN_PIXELS: - 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()); - case dcp::VerificationNote::PICTURE_ASSET_INVALID_FRAME_RATE_FOR_2K: - 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()); - case dcp::VerificationNote::PICTURE_ASSET_INVALID_FRAME_RATE_FOR_4K: - return String::compose("A picture asset's frame rate (%1) is not 24fps as required for 4K DCPs by Bv2.1", note.note().get()); - case dcp::VerificationNote::PICTURE_ASSET_4K_3D: - return "3D 4K DCPs are not allowed by Bv2.1"; - case dcp::VerificationNote::CLOSED_CAPTION_XML_TOO_LARGE_IN_BYTES: - return String::compose("The XML for the closed caption asset %1 is longer than the 256KB maximum required by Bv2.1", note.file()->filename()); - case dcp::VerificationNote::TIMED_TEXT_ASSET_TOO_LARGE_IN_BYTES: - 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()); - case dcp::VerificationNote::TIMED_TEXT_FONTS_TOO_LARGE_IN_BYTES: - 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()); - case dcp::VerificationNote::MISSING_SUBTITLE_LANGUAGE: - return String::compose("The XML for a SMPTE subtitle asset has no tag, which is required by Bv2.1", note.file()->filename()); - case dcp::VerificationNote::SUBTITLE_LANGUAGES_DIFFER: - return String::compose("Some subtitle assets have different tags than others", note.file()->filename()); - case dcp::VerificationNote::MISSING_SUBTITLE_START_TIME: - return String::compose("The XML for a SMPTE subtitle asset has no tag, which is required by Bv2.1", note.file()->filename()); - case dcp::VerificationNote::SUBTITLE_START_TIME_NON_ZERO: - return String::compose("The XML for a SMPTE subtitle asset has a non-zero tag, which is disallowed by Bv2.1", note.file()->filename()); - case dcp::VerificationNote::FIRST_TEXT_TOO_EARLY: + case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS: + return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename()); + case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K: + return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename()); + case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K: + return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename()); + case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D: + return "3D 4K DCPs are not allowed."; + case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES: + return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename()); + case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES: + return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename()); + case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES: + 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()); + case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE: + return String::compose("The XML for the SMPTE subtitle asset %1 has no tag.", note.file()->filename()); + case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES: + return "Some subtitle assets have different tags than others"; + case VerificationNote::Code::MISSING_SUBTITLE_START_TIME: + return String::compose("The XML for the SMPTE subtitle asset %1 has no tag.", note.file()->filename()); + case VerificationNote::Code::INVALID_SUBTITLE_START_TIME: + return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero tag.", note.file()->filename()); + case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME: return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP."; - case dcp::VerificationNote::SUBTITLE_TOO_SHORT: - return "At least one subtitle is less than the minimum of 15 frames suggested by Bv2.1"; - case dcp::VerificationNote::SUBTITLE_TOO_CLOSE: - return "At least one pair of subtitles are separated by less than the the minimum of 2 frames suggested by Bv2.1"; - case dcp::VerificationNote::TOO_MANY_SUBTITLE_LINES: - return "There are more than 3 subtitle lines in at least one place in the DCP, which Bv2.1 advises against."; - case dcp::VerificationNote::SUBTITLE_LINE_LONGER_THAN_RECOMMENDED: - return "There are more than 52 characters in at least one subtitle line, which Bv2.1 advises against."; - case dcp::VerificationNote::SUBTITLE_LINE_TOO_LONG: - return "There are more than 79 characters in at least one subtitle line, which Bv2.1 strongly advises against."; - case dcp::VerificationNote::TOO_MANY_CLOSED_CAPTION_LINES: - return "There are more than 3 closed caption lines in at least one place, which is disallowed by Bv2.1"; - case dcp::VerificationNote::CLOSED_CAPTION_LINE_TOO_LONG: - return "There are more than 32 characters in at least one closed caption line, which is disallowed by Bv2.1"; - case dcp::VerificationNote::INVALID_SOUND_FRAME_RATE: - return "A sound asset has a sampling rate other than 48kHz, which is disallowed by Bv2.1"; - case dcp::VerificationNote::MISSING_ANNOTATION_TEXT_IN_CPL: - return "The CPL has no tag, which is required by Bv2.1"; - case dcp::VerificationNote::CPL_ANNOTATION_TEXT_DIFFERS_FROM_CONTENT_TITLE_TEXT: - return "The CPL's differs from its , which Bv2.1 advises against."; - case dcp::VerificationNote::MISMATCHED_ASSET_DURATION: - return "All assets in a reel do not have the same duration, which is required by Bv2.1"; - case dcp::VerificationNote::MAIN_SUBTITLE_NOT_IN_ALL_REELS: + case VerificationNote::Code::INVALID_SUBTITLE_DURATION: + return "At least one subtitle lasts less than 15 frames."; + case VerificationNote::Code::INVALID_SUBTITLE_SPACING: + return "At least one pair of subtitles is separated by less than 2 frames."; + case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT: + return "There are more than 3 subtitle lines in at least one place in the DCP."; + case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH: + return "There are more than 52 characters in at least one subtitle line."; + case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH: + return "There are more than 79 characters in at least one subtitle line."; + case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT: + return "There are more than 3 closed caption lines in at least one place."; + case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH: + return "There are more than 32 characters in at least one closed caption line."; + case VerificationNote::Code::INVALID_SOUND_FRAME_RATE: + return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get()); + case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT: + return String::compose("The CPL %1 has no tag.", note.note().get()); + case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT: + return String::compose("The CPL %1 has an which differs from its ", note.note().get()); + case VerificationNote::Code::MISMATCHED_ASSET_DURATION: + return "All assets in a reel do not have the same duration."; + case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS: return "At least one reel contains a subtitle asset, but some reel(s) do not"; - case dcp::VerificationNote::CLOSED_CAPTION_ASSET_COUNTS_DIFFER: + case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS: return "At least one reel has closed captions, but reels have different numbers of closed caption assets."; - case dcp::VerificationNote::MISSING_SUBTITLE_ENTRY_POINT: - return "Subtitle assets must have an tag."; - case dcp::VerificationNote::SUBTITLE_ENTRY_POINT_NON_ZERO: - return "Subtitle assets must have an of 0."; - case dcp::VerificationNote::MISSING_CLOSED_CAPTION_ENTRY_POINT: - return "Closed caption assets must have an tag."; - case dcp::VerificationNote::CLOSED_CAPTION_ENTRY_POINT_NON_ZERO: - return "Closed caption assets must have an of 0."; - case dcp::VerificationNote::MISSING_HASH: - return String::compose("An asset is missing a tag: %1", note.note().get()); - case dcp::VerificationNote::MISSING_FFEC_IN_FEATURE: + case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT: + return String::compose("The subtitle asset %1 has no tag.", note.note().get()); + case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT: + return String::compose("The subtitle asset %1 has an other than 0.", note.note().get()); + case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT: + return String::compose("The closed caption asset %1 has no tag.", note.note().get()); + case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT: + return String::compose("The closed caption asset %1 has an other than 0.", note.note().get()); + case VerificationNote::Code::MISSING_HASH: + return String::compose("The asset %1 has no tag in the CPL.", note.note().get()); + case VerificationNote::Code::MISSING_FFEC_IN_FEATURE: return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker"; - case dcp::VerificationNote::MISSING_FFMC_IN_FEATURE: + case VerificationNote::Code::MISSING_FFMC_IN_FEATURE: return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker"; - case dcp::VerificationNote::MISSING_FFOC: + case VerificationNote::Code::MISSING_FFOC: return "There should be a FFOC (first frame of content) marker"; - case dcp::VerificationNote::MISSING_LFOC: + case VerificationNote::Code::MISSING_LFOC: return "There should be a LFOC (last frame of content) marker"; - case dcp::VerificationNote::INCORRECT_FFOC: - return "The FFOC marker should bet set to 1"; - case dcp::VerificationNote::INCORRECT_LFOC: - return "The LFOC marker should be set to 1 less than the duration of the last reel"; + case VerificationNote::Code::INCORRECT_FFOC: + return String::compose("The FFOC marker is %1 instead of 1", note.note().get()); + case VerificationNote::Code::INCORRECT_LFOC: + return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get()); + case VerificationNote::Code::MISSING_CPL_METADATA: + return String::compose("The CPL %1 has no tag.", note.note().get()); + case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER: + return String::compose("The CPL %1 has no in its .", note.note().get()); + case VerificationNote::Code::MISSING_EXTENSION_METADATA: + return String::compose("The CPL %1 has no in its .", note.note().get()); + case VerificationNote::Code::INVALID_EXTENSION_METADATA: + return String::compose("The CPL %1 has a malformed (%2).", note.file()->filename(), note.note().get()); + case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT: + return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get()); + case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT: + return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get()); + case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL: + return String::compose("The PKL %1 has only one CPL but its does not match the CPL's .", note.note().get()); + case VerificationNote::Code::PARTIALLY_ENCRYPTED: + return "Some assets are encrypted but some are not."; + case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM: + return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1)", note.note().get()); + case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K: + return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get()); + case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K: + return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get()); + case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE: + return "The JPEG2000 tile size is not the same as the image size."; + case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH: + return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get()); + case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT: + return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get()); + case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K: + return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get()); + case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K: + return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get()); + case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER: + return String::compose("Incorrect POC marker content found (%1)", note.note().get()); + case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION: + return "POC marker found outside main header"; + case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K: + return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get()); + case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K: + return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get()); + case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER: + return "No TLM marker was found in a JPEG2000 codestream."; } return ""; } + + +bool +dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b) +{ + return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line(); +} + + +std::ostream& +dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note) +{ + s << note_to_string (note); + if (note.note()) { + s << " [" << note.note().get() << "]"; + } + if (note.file()) { + s << " [" << note.file().get() << "]"; + } + if (note.line()) { + s << " [" << note.line().get() << "]"; + } + return s; +} +