+
+static void
+verify_main_picture_asset (
+ shared_ptr<const DCP> dcp,
+ shared_ptr<const ReelPictureAsset> reel_asset,
+ function<void (string, optional<boost::filesystem::path>)> stage,
+ function<void (float)> progress,
+ vector<VerificationNote>& 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;
+ }
+ stage ("Checking picture frame sizes", asset->file());
+ auto const pr = verify_picture_asset (reel_asset, progress);
+ switch (pr) {
+ case VerifyPictureAssetResult::BAD:
+ notes.push_back ({
+ VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
+ });
+ break;
+ 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;
+ }
+
+ /* Only flat/scope allowed by Bv2.1 */
+ if (
+ 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() == 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::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() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
+ /* Only 24fps allowed for 4K */
+ 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<const StereoPictureAsset>(asset)) {
+ 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
+ });
+
+ }
+ }
+
+}
+
+
+static void
+verify_main_sound_asset (
+ shared_ptr<const DCP> dcp,
+ shared_ptr<const ReelSoundAsset> reel_asset,
+ function<void (string, optional<boost::filesystem::path>)> stage,
+ function<void (float)> progress,
+ vector<VerificationNote>& notes
+ )
+{
+ 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;
+ }
+
+ stage ("Checking sound asset metadata", asset->file());
+
+ verify_language_tag (asset->language(), notes);
+ if (asset->sampling_rate() != 48000) {
+ notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), *asset->file()});
+ }
+}
+
+
+static void
+verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
+{
+ /* XXX: is Language compulsory? */
+ if (reel_asset->language()) {
+ verify_language_tag (*reel_asset->language(), notes);
+ }
+
+ if (!reel_asset->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::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
+ }
+}
+
+
+static void
+verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
+{
+ /* XXX: is Language compulsory? */
+ if (reel_asset->language()) {
+ verify_language_tag (*reel_asset->language(), notes);
+ }
+
+ if (!reel_asset->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::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
+ }
+}
+
+
+struct State
+{
+ boost::optional<string> subtitle_language;
+};
+
+
+/** Verify stuff that is common to both subtitles and closed captions */
+void
+verify_smpte_timed_text_asset (
+ shared_ptr<const SMPTESubtitleAsset> asset,
+ vector<VerificationNote>& notes
+ )
+{
+ if (asset->language()) {
+ verify_language_tag (*asset->language(), notes);
+ } else {
+ notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
+ }
+
+ auto const size = boost::filesystem::file_size(asset->file().get());
+ if (size > 115 * 1024 * 1024) {
+ notes.push_back (
+ { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(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"
+ * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
+ */
+ auto fonts = asset->font_data ();
+ int total_size = 0;
+ for (auto i: fonts) {
+ total_size += i.second.size();
+ }
+ if (total_size > 10 * 1024 * 1024) {
+ notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
+ }
+
+ if (!asset->start_time()) {
+ 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() });
+ }
+}
+
+
+/** Verify SMPTE subtitle-only stuff */
+void
+verify_smpte_subtitle_asset (
+ shared_ptr<const SMPTESubtitleAsset> asset,
+ vector<VerificationNote>& notes,
+ State& state
+ )
+{
+ if (asset->language()) {
+ if (!state.subtitle_language) {
+ state.subtitle_language = *asset->language();
+ } else if (state.subtitle_language != *asset->language()) {
+ notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
+ }
+ }
+}
+
+
+/** Verify all subtitle stuff */
+static void
+verify_subtitle_asset (
+ shared_ptr<const SubtitleAsset> asset,
+ function<void (string, optional<boost::filesystem::path>)> stage,
+ boost::filesystem::path xsd_dtd_directory,
+ vector<VerificationNote>& notes,
+ State& state
+ )
+{
+ stage ("Checking subtitle XML", asset->file());
+ /* 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 smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
+ if (smpte) {
+ verify_smpte_timed_text_asset (smpte, notes);
+ verify_smpte_subtitle_asset (smpte, notes, state);
+ }
+}
+
+
+/** Verify all closed caption stuff */
+static void
+verify_closed_caption_asset (
+ shared_ptr<const SubtitleAsset> asset,
+ function<void (string, optional<boost::filesystem::path>)> stage,
+ boost::filesystem::path xsd_dtd_directory,
+ vector<VerificationNote>& notes
+ )
+{
+ stage ("Checking closed caption XML", asset->file());
+ /* 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 smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
+ if (smpte) {
+ verify_smpte_timed_text_asset (smpte, 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<string>(asset->raw_xml().size()), *asset->file()});
+ }
+}
+
+
+static
+void
+verify_text_timing (
+ vector<shared_ptr<Reel>> reels,
+ int edit_rate,
+ vector<VerificationNote>& notes,
+ std::function<bool (shared_ptr<Reel>)> check,
+ std::function<string (shared_ptr<Reel>)> xml,
+ std::function<int64_t (shared_ptr<Reel>)> duration
+ )
+{
+ /* end of last subtitle (in editable units) */
+ optional<int64_t> last_out;
+ auto too_short = false;
+ auto too_close = false;
+ auto too_early = false;
+ auto reel_overlap = false;
+ /* current reel start time (in editable units) */
+ int64_t reel_offset = 0;
+
+ std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool)> parse;
+ parse = [&parse, &last_out, &too_short, &too_close, &too_early, &reel_offset](cxml::ConstNodePtr node, optional<int> tcr, optional<Time> start_time, int er, bool first_reel) {
+ if (node->name() == "Subtitle") {
+ Time in (node->string_attribute("TimeIn"), tcr);
+ if (start_time) {
+ in -= *start_time;
+ }
+ Time out (node->string_attribute("TimeOut"), tcr);
+ if (start_time) {
+ out -= *start_time;
+ }
+ if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
+ too_early = true;
+ }
+ auto length = out - in;
+ if (length.as_editable_units_ceil(er) < 15) {
+ too_short = true;
+ }
+ if (last_out) {
+ /* XXX: this feels dubious - is it really what Bv2.1 means? */
+ auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
+ if (distance >= 0 && distance < 2) {
+ too_close = true;
+ }
+ }
+ last_out = reel_offset + out.as_editable_units_floor(er);
+ } else {
+ for (auto i: node->node_children()) {
+ parse(i, tcr, start_time, er, first_reel);
+ }
+ }
+ };
+
+ for (auto i = 0U; i < reels.size(); ++i) {
+ if (!check(reels[i])) {
+ continue;
+ }
+
+ /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
+ * read in by libdcp's parser.
+ */
+
+ shared_ptr<cxml::Document> doc;
+ optional<int> tcr;
+ optional<Time> start_time;
+ try {
+ doc = make_shared<cxml::Document>("SubtitleReel");
+ doc->read_string (xml(reels[i]));
+ tcr = doc->number_child<int>("TimeCodeRate");
+ auto start_time_string = doc->optional_string_child("StartTime");
+ if (start_time_string) {
+ start_time = Time(*start_time_string, tcr);
+ }
+ } catch (...) {
+ doc = make_shared<cxml::Document>("DCSubtitle");
+ doc->read_string (xml(reels[i]));
+ }
+ parse (doc, tcr, start_time, edit_rate, i == 0);
+ auto end = reel_offset + duration(reels[i]);
+ if (last_out && *last_out > end) {
+ reel_overlap = true;
+ }
+ reel_offset = end;
+ }
+
+ if (last_out && *last_out > reel_offset) {
+ reel_overlap = true;
+ }
+
+ if (too_early) {
+ notes.push_back({
+ VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
+ });
+ }
+
+ if (too_short) {
+ notes.push_back ({
+ VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
+ });
+ }
+
+ if (too_close) {
+ notes.push_back ({
+ VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
+ });
+ }
+
+ if (reel_overlap) {
+ notes.push_back ({
+ VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
+ });
+ }
+}
+
+
+struct LinesCharactersResult
+{
+ bool warning_length_exceeded = false;
+ bool error_length_exceeded = false;
+ bool line_count_exceeded = false;
+};
+
+
+static
+void
+verify_text_lines_and_characters (
+ shared_ptr<SubtitleAsset> asset,
+ int warning_length,
+ int error_length,
+ LinesCharactersResult* result
+ )
+{
+ class Event
+ {
+ public:
+ Event (Time time_, float position_, int characters_)
+ : time (time_)
+ , position (position_)
+ , characters (characters_)
+ {}
+
+ Event (Time time_, shared_ptr<Event> start_)
+ : time (time_)
+ , start (start_)
+ {}
+
+ Time time;
+ int position; //< position from 0 at top of screen to 100 at bottom
+ int characters;
+ shared_ptr<Event> start;
+ };
+
+ vector<shared_ptr<Event>> events;
+
+ auto position = [](shared_ptr<const SubtitleString> sub) {
+ switch (sub->v_align()) {
+ case VAlign::TOP:
+ return lrintf(sub->v_position() * 100);
+ case VAlign::CENTER:
+ return lrintf((0.5f + sub->v_position()) * 100);
+ case VAlign::BOTTOM:
+ return lrintf((1.0f - sub->v_position()) * 100);
+ }
+
+ return 0L;
+ };
+
+ for (auto j: asset->subtitles()) {
+ auto text = dynamic_pointer_cast<const SubtitleString>(j);
+ if (text) {
+ auto in = make_shared<Event>(text->in(), position(text), text->text().length());
+ events.push_back(in);
+ events.push_back(make_shared<Event>(text->out(), in));
+ }
+ }
+
+ std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
+ return a->time < b->time;
+ });
+
+ map<int, int> current;
+ for (auto i: events) {
+ if (current.size() > 3) {
+ result->line_count_exceeded = true;
+ }
+ for (auto j: current) {
+ if (j.second >= warning_length) {
+ result->warning_length_exceeded = true;
+ }
+ if (j.second >= error_length) {
+ result->error_length_exceeded = true;
+ }
+ }
+
+ if (i->start) {
+ /* end of a subtitle */
+ DCP_ASSERT (current.find(i->start->position) != current.end());
+ if (current[i->start->position] == i->start->characters) {
+ current.erase(i->start->position);
+ } else {
+ current[i->start->position] -= i->start->characters;
+ }
+ } else {
+ /* start of a subtitle */
+ if (current.find(i->position) == current.end()) {
+ current[i->position] = i->characters;
+ } else {
+ current[i->position] += i->characters;
+ }
+ }
+ }
+}
+
+
+static
+void
+verify_text_timing (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
+{
+ if (reels.empty()) {
+ return;
+ }
+
+ if (reels[0]->main_subtitle()) {
+ verify_text_timing (reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
+ [](shared_ptr<Reel> reel) {
+ return static_cast<bool>(reel->main_subtitle());
+ },
+ [](shared_ptr<Reel> reel) {
+ return reel->main_subtitle()->asset()->raw_xml();
+ },
+ [](shared_ptr<Reel> reel) {
+ return reel->main_subtitle()->actual_duration();
+ }
+ );
+ }
+
+ for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
+ verify_text_timing (reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
+ [i](shared_ptr<Reel> reel) {
+ return i < reel->closed_captions().size();
+ },
+ [i](shared_ptr<Reel> reel) {
+ return reel->closed_captions()[i]->asset()->raw_xml();
+ },
+ [i](shared_ptr<Reel> reel) {
+ return reel->closed_captions()[i]->actual_duration();
+ }
+ );
+ }
+}
+
+
+void
+verify_extension_metadata (shared_ptr<CPL> cpl, vector<VerificationNote>& 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 = "<Name> 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 = "<Name> property should be 'DCP Constraints Profile'";
+ }
+ }
+ if (auto value = property->optional_node_child("Value")) {
+ if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
+ malformed = "<Value> 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> dcp, shared_ptr<PKL> pkl)
+{
+ vector<string> 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<MXF>(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<VerificationNote>
+dcp::verify (
+ vector<boost::filesystem::path> directories,
+ function<void (string, optional<boost::filesystem::path>)> stage,
+ function<void (float)> progress,
+ boost::filesystem::path xsd_dtd_directory
+ )
+{
+ xsd_dtd_directory = boost::filesystem::canonical (xsd_dtd_directory);
+
+ vector<VerificationNote> notes;
+ State state{};
+
+ vector<shared_ptr<DCP>> dcps;
+ for (auto i: directories) {