X-Git-Url: https://git.carlh.net/gitweb/?a=blobdiff_plain;f=src%2Fverify.cc;h=42ee192119b85280ada83512167a01aaa2a4805d;hb=4dca630057509164494b65c2deeb748a51928c73;hp=e176362aeae3a78e98056eb9842cb6c409607211;hpb=7d66bda50ade8ea618f331b885f1bfa4fa0a2af9;p=libdcp.git diff --git a/src/verify.cc b/src/verify.cc index e176362a..42ee1921 100644 --- a/src/verify.cc +++ b/src/verify.cc @@ -58,6 +58,7 @@ #include "stereo_picture_frame.h" #include "verify.h" #include "verify_j2k.h" +#include #include #include #include @@ -79,21 +80,23 @@ #include #include #include +#include +#include #include -using std::list; -using std::vector; -using std::string; using std::cout; +using std::dynamic_pointer_cast; +using std::list; +using std::make_shared; using std::map; using std::max; using std::set; using std::shared_ptr; -using std::make_shared; +using std::string; +using std::vector; using boost::optional; using boost::function; -using std::dynamic_pointer_cast; using namespace dcp; @@ -156,22 +159,22 @@ private: class DCPErrorHandler : public ErrorHandler { public: - void warning(const SAXParseException& e) + void warning(const SAXParseException& e) override { maybe_add (XMLValidationError(e)); } - void error(const SAXParseException& e) + void error(const SAXParseException& e) override { maybe_add (XMLValidationError(e)); } - void fatalError(const SAXParseException& e) + void fatalError(const SAXParseException& e) override { maybe_add (XMLValidationError(e)); } - void resetErrors() { + void resetErrors() override { _errors.clear (); } @@ -240,13 +243,14 @@ public: add("http://www.digicine.com/PROTO-ASDCP-AM-20040311.xsd", "PROTO-ASDCP-AM-20040311.xsd"); add("http://www.digicine.com/PROTO-ASDCP-CC-CPL-20070926#", "PROTO-ASDCP-CC-CPL-20070926.xsd"); add("interop-subs", "DCSubtitle.v1.mattsson.xsd"); - add("http://www.smpte-ra.org/schemas/428-7/2010/DCST.xsd", "SMPTE-428-7-2010-DCST.xsd"); + add("http://www.smpte-ra.org/schemas/428-7/2010/DCST.xsd", "DCDMSubtitle-2010.xsd"); + add("http://www.smpte-ra.org/schemas/428-7/2014/DCST.xsd", "DCDMSubtitle-2014.xsd"); add("http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata", "SMPTE-429-16.xsd"); add("http://www.dolby.com/schemas/2012/AD", "Dolby-2012-AD.xsd"); add("http://www.smpte-ra.org/schemas/429-10/2008/Main-Stereo-Picture-CPL", "SMPTE-429-10-2008.xsd"); } - InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id) + InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id) override { if (!system_id) { return 0; @@ -276,7 +280,7 @@ private: static void parse (XercesDOMParser& parser, boost::filesystem::path xml) { - parser.parse(xml.string().c_str()); + parser.parse(xml.c_str()); } @@ -319,6 +323,7 @@ validate_xml (T xml, boost::filesystem::path xsd_dtd_directory, vector dcp, shared_ptr reel_file_asset, function progress) { + /* When reading the DCP the hash will have been set to the one from the PKL/CPL. + * We want to calculate the hash of the actual file contents here, so that we + * can check it. unset_hash() means that this calculation will happen on the + * call to hash(). + */ + reel_file_asset->asset_ref()->unset_hash(); auto const actual_hash = reel_file_asset->asset_ref()->hash(progress); auto pkls = dcp->pkls(); @@ -445,7 +456,7 @@ verify_picture_asset (shared_ptr reel_file_asset, boost::fi biggest_frame = max(biggest_frame, frame->size()); if (!mono_asset->encrypted() || mono_asset->key()) { vector j2k_notes; - verify_j2k (frame, j2k_notes); + verify_j2k(frame, i, mono_asset->frame_rate().numerator, j2k_notes); check_and_add (j2k_notes); } progress (float(i) / duration); @@ -455,10 +466,10 @@ verify_picture_asset (shared_ptr reel_file_asset, boost::fi for (int64_t i = 0; i < duration; ++i) { auto frame = reader->get_frame (i); biggest_frame = max(biggest_frame, max(frame->left()->size(), frame->right()->size())); - if (!stereo_asset->encrypted() || mono_asset->key()) { + if (!stereo_asset->encrypted() || stereo_asset->key()) { vector j2k_notes; - verify_j2k (frame->left(), j2k_notes); - verify_j2k (frame->right(), j2k_notes); + verify_j2k(frame->left(), i, stereo_asset->frame_rate().numerator, j2k_notes); + verify_j2k(frame->right(), i, stereo_asset->frame_rate().numerator, j2k_notes); check_and_add (j2k_notes); } progress (float(i) / duration); @@ -486,27 +497,32 @@ verify_main_picture_asset ( shared_ptr reel_asset, function)> stage, function progress, + VerificationOptions options, vector& notes ) { auto asset = reel_asset->asset(); auto const file = *asset->file(); - stage ("Checking picture asset hash", file); - auto const r = verify_asset (dcp, reel_asset, progress); - switch (r) { - case VerifyAssetResult::BAD: - notes.push_back ({ - VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file - }); - break; - case VerifyAssetResult::CPL_PKL_DIFFER: - notes.push_back ({ - VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file - }); - break; - default: - break; + + if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || boost::filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) { + stage ("Checking picture asset hash", file); + auto const r = verify_asset (dcp, reel_asset, progress); + switch (r) { + case VerifyAssetResult::BAD: + notes.push_back ({ + VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file + }); + break; + case VerifyAssetResult::CPL_PKL_DIFFER: + notes.push_back ({ + VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file + }); + break; + default: + break; + } } + stage ("Checking picture frame sizes", asset->file()); verify_picture_asset (reel_asset, file, notes, progress); @@ -563,36 +579,55 @@ verify_main_picture_asset ( } +struct State +{ + boost::optional subtitle_language; + boost::optional audio_channels; +}; + + static void verify_main_sound_asset ( shared_ptr dcp, shared_ptr reel_asset, function)> stage, function progress, - vector& notes + VerificationOptions options, + vector& notes, + State& state ) { auto asset = reel_asset->asset(); - stage ("Checking sound asset hash", asset->file()); - auto const r = verify_asset (dcp, reel_asset, progress); - switch (r) { - case VerifyAssetResult::BAD: - notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, *asset->file()}); - break; - case VerifyAssetResult::CPL_PKL_DIFFER: - notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, *asset->file()}); - break; - default: - break; + auto const file = *asset->file(); + + if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || boost::filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) { + stage("Checking sound asset hash", file); + auto const r = verify_asset (dcp, reel_asset, progress); + switch (r) { + case VerifyAssetResult::BAD: + notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, file}); + break; + case VerifyAssetResult::CPL_PKL_DIFFER: + notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, file}); + break; + default: + break; + } + } + + if (!state.audio_channels) { + state.audio_channels = asset->channels(); + } else if (*state.audio_channels != asset->channels()) { + notes.push_back({ VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS, file }); } - stage ("Checking sound asset metadata", asset->file()); + stage ("Checking sound asset metadata", file); if (auto lang = asset->language()) { verify_language_tag (*lang, notes); } if (asset->sampling_rate() != 48000) { - notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert(asset->sampling_rate()), *asset->file()}); + notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert(asset->sampling_rate()), file}); } } @@ -629,12 +664,6 @@ verify_closed_caption_reel (shared_ptr reel_asset, } -struct State -{ - boost::optional subtitle_language; -}; - - /** Verify stuff that is common to both subtitles and closed captions */ void verify_smpte_timed_text_asset ( @@ -686,6 +715,20 @@ verify_smpte_timed_text_asset ( } +/** Verify Interop subtitle-only stuff */ +void +verify_interop_subtitle_asset(shared_ptr asset, vector& notes) +{ + if (asset->subtitles().empty()) { + notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_SUBTITLE, asset->id(), asset->file().get() }); + } + auto const unresolved = asset->unresolved_fonts(); + if (!unresolved.empty()) { + notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_FONT, unresolved.front() }); + } +} + + /** Verify SMPTE subtitle-only stuff */ void verify_smpte_subtitle_asset ( @@ -703,12 +746,28 @@ verify_smpte_subtitle_asset ( } DCP_ASSERT (asset->resource_id()); - if (asset->resource_id().get() != asset->xml_id()) { - notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID }); + auto xml_id = asset->xml_id(); + if (xml_id) { + if (asset->resource_id().get() != xml_id) { + notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID }); + } + + if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) { + notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID }); + } + } else { + notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED}); } - if (asset->id() == asset->resource_id().get() || asset->id() == asset->xml_id()) { - notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID }); + if (asset->raw_xml()) { + /* Deluxe require this in their QC even if it seems never to be mentioned in any standard */ + cxml::Document doc("SubtitleReel"); + doc.read_string(*asset->raw_xml()); + auto issue_date = doc.string_child("IssueDate"); + std::regex reg("^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d$"); + if (!std::regex_match(issue_date, reg)) { + notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, issue_date}); + } } } @@ -728,12 +787,39 @@ verify_subtitle_asset ( /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk * gets passed through libdcp which may clean up and therefore hide errors. */ - validate_xml (asset->raw_xml(), xsd_dtd_directory, notes); + if (asset->raw_xml()) { + validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes); + } else { + notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED}); + } + + auto namespace_count = [](shared_ptr asset, string root_node) { + cxml::Document doc(root_node); + doc.read_string(asset->raw_xml().get()); + auto root = dynamic_cast(doc.node())->cobj(); + int count = 0; + for (auto ns = root->nsDef; ns != nullptr; ns = ns->next) { + ++count; + } + return count; + }; + + auto interop = dynamic_pointer_cast(asset); + if (interop) { + verify_interop_subtitle_asset(interop, notes); + if (namespace_count(asset, "DCSubtitle") > 1) { + notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id() }); + } + } auto smpte = dynamic_pointer_cast(asset); if (smpte) { verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes); verify_smpte_subtitle_asset (smpte, notes, state); + /* This asset may be encrypted and in that case we'll have no raw_xml() */ + if (asset->raw_xml() && namespace_count(asset, "SubtitleReel") > 1) { + notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id()}); + } } } @@ -752,28 +838,35 @@ verify_closed_caption_asset ( /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk * gets passed through libdcp which may clean up and therefore hide errors. */ - validate_xml (asset->raw_xml(), xsd_dtd_directory, notes); + auto raw_xml = asset->raw_xml(); + if (raw_xml) { + validate_xml (*raw_xml, xsd_dtd_directory, notes); + if (raw_xml->size() > 256 * 1024) { + notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert(raw_xml->size()), *asset->file()}); + } + } else { + notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED}); + } auto smpte = dynamic_pointer_cast(asset); if (smpte) { verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes); } - - if (asset->raw_xml().size() > 256 * 1024) { - notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert(asset->raw_xml().size()), *asset->file()}); - } } +/** Check the timing of the individual subtitles and make sure there are no empty nodes etc. */ static void -verify_text_timing ( +verify_text_details ( + dcp::Standard standard, vector> reels, int edit_rate, vector& notes, std::function)> check, - std::function)> xml, - std::function)> duration + std::function (shared_ptr)> xml, + std::function)> duration, + std::function)> id ) { /* end of last subtitle (in editable units) */ @@ -782,11 +875,22 @@ verify_text_timing ( auto too_close = false; auto too_early = false; auto reel_overlap = false; + auto empty_text = false; /* current reel start time (in editable units) */ int64_t reel_offset = 0; - - std::function, optional