using std::map;
using std::max;
using std::shared_ptr;
+using std::make_shared;
using boost::optional;
using boost::function;
using std::dynamic_pointer_cast;
};
+
+void
+verify_smpte_subtitle_asset (
+ shared_ptr<const dcp::SMPTESubtitleAsset> asset,
+ vector<VerificationNote>& notes,
+ State& state
+ )
+{
+ if (asset->language()) {
+ auto const language = *asset->language();
+ verify_language_tag (language, notes);
+ 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()
+ )
+ );
+ }
+ } else {
+ notes.push_back (
+ VerificationNote(
+ VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_SUBTITLE_LANGUAGE, *asset->file()
+ )
+ );
+ }
+ if (boost::filesystem::file_size(*asset->file()) > 115 * 1024 * 1024) {
+ notes.push_back (
+ VerificationNote(
+ VerificationNote::VERIFY_BV21_ERROR, VerificationNote::TIMED_TEXT_ASSET_TOO_LARGE_IN_BYTES, *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(
+ VerificationNote::VERIFY_BV21_ERROR, VerificationNote::TIMED_TEXT_FONTS_TOO_LARGE_IN_BYTES, *asset->file()
+ )
+ );
+ }
+
+ 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())
+ );
+ }
+}
+
+
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,
- bool first_reel
+ State& state
)
{
stage ("Checking subtitle XML", asset->file());
auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
if (smpte) {
- if (smpte->language()) {
- auto const language = *smpte->language();
- verify_language_tag (language, notes);
- 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()
- )
- );
- }
- } else {
- notes.push_back (
- VerificationNote(
- VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_SUBTITLE_LANGUAGE, *asset->file()
- )
- );
- }
- if (boost::filesystem::file_size(*asset->file()) > 115 * 1024 * 1024) {
- notes.push_back (
- VerificationNote(
- VerificationNote::VERIFY_BV21_ERROR, VerificationNote::TIMED_TEXT_ASSET_TOO_LARGE_IN_BYTES, *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(
- VerificationNote::VERIFY_BV21_ERROR, VerificationNote::TIMED_TEXT_FONTS_TOO_LARGE_IN_BYTES, *asset->file()
- )
- );
- }
-
- if (!smpte->start_time()) {
- notes.push_back (
- VerificationNote(
- VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_SUBTITLE_START_TIME, *asset->file())
- );
- } else if (smpte->start_time() != dcp::Time()) {
- notes.push_back (
- VerificationNote(
- VerificationNote::VERIFY_BV21_ERROR, VerificationNote::SUBTITLE_START_TIME_NON_ZERO, *asset->file())
- );
- }
-
- if (first_reel) {
- auto subs = smpte->subtitles();
- sort (subs.begin(), subs.end(), [](shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
- return a->in() < b->in();
- });
- if (!subs.empty() && subs.front()->in() < dcp::Time(0, 0, 4, 0, 24)) {
- notes.push_back(
- VerificationNote(
- VerificationNote::VERIFY_WARNING, VerificationNote::FIRST_TEXT_TOO_EARLY
- )
- );
- }
- }
+ verify_smpte_subtitle_asset (smpte, notes, state);
}
}
function<void (string, optional<boost::filesystem::path>)> stage,
boost::filesystem::path xsd_dtd_directory,
vector<VerificationNote>& notes,
- State& state,
- bool first_reel
+ State& state
)
{
- verify_subtitle_asset (asset, stage, xsd_dtd_directory, notes, state, first_reel);
+ verify_subtitle_asset (asset, stage, xsd_dtd_directory, notes, state);
if (asset->raw_xml().size() > 256 * 1024) {
notes.push_back (
}
+static
+void
+check_text_timing (
+ vector<shared_ptr<dcp::Reel>> reels,
+ optional<int> picture_frame_rate,
+ vector<VerificationNote>& notes,
+ std::function<std::string (shared_ptr<dcp::Reel>)> xml,
+ std::function<int64_t (shared_ptr<dcp::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;
+ /* current reel start time (in editable units) */
+ int64_t reel_offset = 0;
+
+ std::function<void (cxml::ConstNodePtr, int, int, bool)> 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)) {
+ too_early = true;
+ }
+ auto length = out - in;
+ if (length.as_editable_units(pfr) < 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(pfr) - *last_out;
+ if (distance >= 0 && distance < 2) {
+ too_close = true;
+ }
+ }
+ last_out = reel_offset + out.as_editable_units(pfr);
+ } else {
+ for (auto i: node->node_children()) {
+ parse(i, tcr, pfr, first_reel);
+ }
+ }
+ };
+
+ for (auto i = 0U; i < reels.size(); ++i) {
+ /* 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.
+ */
+
+ auto doc = make_shared<cxml::Document>("SubtitleReel");
+ doc->read_string (xml(reels[i]));
+ auto const tcr = doc->number_child<int>("TimeCodeRate");
+ parse (doc, tcr, picture_frame_rate.get_value_or(24), i == 0);
+ reel_offset += duration(reels[i]);
+ }
+
+ if (too_early) {
+ notes.push_back(
+ VerificationNote(
+ VerificationNote::VERIFY_WARNING, VerificationNote::FIRST_TEXT_TOO_EARLY
+ )
+ );
+ }
+
+ if (too_short) {
+ notes.push_back (
+ VerificationNote(
+ VerificationNote::VERIFY_WARNING, VerificationNote::SUBTITLE_TOO_SHORT
+ )
+ );
+ }
+
+ if (too_close) {
+ notes.push_back (
+ VerificationNote(
+ VerificationNote::VERIFY_WARNING, VerificationNote::SUBTITLE_TOO_CLOSE
+ )
+ );
+ }
+}
+
+
+static
+void
+check_text_timing (vector<shared_ptr<dcp::Reel>> reels, vector<VerificationNote>& notes)
+{
+ if (reels.empty()) {
+ return;
+ }
+
+ optional<int> picture_frame_rate;
+ if (reels[0]->main_picture()) {
+ picture_frame_rate = reels[0]->main_picture()->frame_rate().numerator;
+ }
+
+ if (reels[0]->main_subtitle()) {
+ check_text_timing (reels, picture_frame_rate, notes,
+ [](shared_ptr<dcp::Reel> reel) {
+ return reel->main_subtitle()->asset()->raw_xml();
+ },
+ [](shared_ptr<dcp::Reel> 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<dcp::Reel> reel) {
+ return reel->closed_captions()[i]->asset()->raw_xml();
+ },
+ [i](shared_ptr<dcp::Reel> reel) {
+ return reel->closed_captions()[i]->actual_duration();
+ }
+ );
+ }
+}
+
+
vector<VerificationNote>
dcp::verify (
vector<boost::filesystem::path> directories,
}
}
- bool first_reel = true;
for (auto reel: cpl->reels()) {
stage ("Checking reel", optional<boost::filesystem::path>());
if (reel->main_subtitle()) {
verify_main_subtitle_reel (reel->main_subtitle(), notes);
if (reel->main_subtitle()->asset_ref().resolved()) {
- verify_subtitle_asset (reel->main_subtitle()->asset(), stage, xsd_dtd_directory, notes, state, first_reel);
+ verify_subtitle_asset (reel->main_subtitle()->asset(), stage, xsd_dtd_directory, notes, state);
}
}
for (auto i: reel->closed_captions()) {
verify_closed_caption_reel (i, notes);
if (i->asset_ref().resolved()) {
- verify_closed_caption_asset (i->asset(), stage, xsd_dtd_directory, notes, state, first_reel);
+ verify_closed_caption_asset (i->asset(), stage, xsd_dtd_directory, notes, state);
}
}
+ }
- first_reel = false;
+ if (dcp->standard() == dcp::SMPTE) {
+ check_text_timing (cpl->reels(), notes);
}
}
return String::compose("The XML for a SMPTE subtitle asset has a non-zero <StartTime> tag, which is disallowed by Bv2.1", note.file()->filename());
case dcp::VerificationNote::FIRST_TEXT_TOO_EARLY:
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";
}
return "";
check_verify_result (vector<boost::filesystem::path> dir, vector<std::pair<dcp::VerificationNote::Type, dcp::VerificationNote::Code>> types_and_codes)
{
auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
+ dump_notes (notes);
BOOST_REQUIRE_EQUAL (notes.size(), types_and_codes.size());
auto i = notes.begin();
auto j = types_and_codes.begin();
auto reel_asset = make_shared<dcp::ReelSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
write_dcp_with_single_asset (dir, reel_asset, dcp::INTEROP);
- check_verify_result (
- { dir },
- {{ dcp::VerificationNote::VERIFY_BV21_ERROR, dcp::VerificationNote::NOT_SMPTE }}
- );
+ check_verify_result ({dir}, {{ dcp::VerificationNote::VERIFY_BV21_ERROR, dcp::VerificationNote::NOT_SMPTE }});
}
write_dcp_with_single_asset (dir, reel_asset);
auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
- dump_notes (notes);
BOOST_REQUIRE_EQUAL (notes.size(), 0);
}
dcp->write_xml (dcp::SMPTE);
auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
- dump_notes (notes);
BOOST_REQUIRE_EQUAL (notes.size(), 4U);
auto i = notes.begin ();
BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::BAD_LANGUAGE);
{ dir },
{
{ dcp::VerificationNote::VERIFY_BV21_ERROR, dcp::VerificationNote::MISSING_SUBTITLE_START_TIME },
- { dcp::VerificationNote::VERIFY_WARNING, dcp::VerificationNote::FIRST_TEXT_TOO_EARLY },
- { dcp::VerificationNote::VERIFY_BV21_ERROR, dcp::VerificationNote::CLOSED_CAPTION_XML_TOO_LARGE_IN_BYTES }
+ { dcp::VerificationNote::VERIFY_BV21_ERROR, dcp::VerificationNote::CLOSED_CAPTION_XML_TOO_LARGE_IN_BYTES },
+ { dcp::VerificationNote::VERIFY_WARNING, dcp::VerificationNote::FIRST_TEXT_TOO_EARLY }
});
}
BOOST_AUTO_TEST_CASE (verify_inconsistent_subtitle_languages)
{
boost::filesystem::path path ("build/test/verify_inconsistent_subtitle_languages");
- auto dcp = make_simple (path, 2);
- auto cpl = dcp->cpls().front();
+ auto dcp = make_simple (path, 2, 240);
+ auto cpl = dcp->cpls()[0];
{
auto subs = make_shared<dcp::SMPTESubtitleAsset>();
subs->add (simple_subtitle());
subs->write (path / "subs1.mxf");
auto reel_subs = make_shared<dcp::ReelSubtitleAsset>(subs, dcp::Fraction(24, 1), 240, 0);
- cpl->reels().front()->add(reel_subs);
+ cpl->reels()[0]->add(reel_subs);
}
{
subs->add (simple_subtitle());
subs->write (path / "subs2.mxf");
auto reel_subs = make_shared<dcp::ReelSubtitleAsset>(subs, dcp::Fraction(24, 1), 240, 0);
- cpl->reels().back()->add(reel_subs);
+ cpl->reels()[1]->add(reel_subs);
}
dcp->write_xml (dcp::SMPTE);
}
-BOOST_AUTO_TEST_CASE (verify_text_too_early)
+static
+void
+dcp_with_subtitles (boost::filesystem::path dir, vector<int> timings)
{
- auto const dir = boost::filesystem::path("build/test/verify_text_too_early");
prepare_directory (dir);
auto asset = make_shared<dcp::SMPTESubtitleAsset>();
asset->set_start_time (dcp::Time());
- /* Just too early */
- add_test_subtitle (asset, 4 * 24 - 1, 5 * 24);
+ for (auto i = 0U; i < timings.size(); i += 2) {
+ add_test_subtitle (asset, timings[i], timings[i + 1]);
+ }
asset->set_language (dcp::LanguageTag("de-DE"));
asset->write (dir / "subs.mxf");
auto reel_asset = make_shared<dcp::ReelSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
write_dcp_with_single_asset (dir, reel_asset);
+}
+
+BOOST_AUTO_TEST_CASE (verify_text_too_early)
+{
+ auto const dir = boost::filesystem::path("build/test/verify_text_too_early");
+ /* Just too early */
+ dcp_with_subtitles (dir, { 4 * 24 - 1, 5 * 24 });
check_verify_result (
{ dir },
- {
- { dcp::VerificationNote::VERIFY_WARNING, dcp::VerificationNote::FIRST_TEXT_TOO_EARLY }
- });
+ {{ dcp::VerificationNote::VERIFY_WARNING, dcp::VerificationNote::FIRST_TEXT_TOO_EARLY }});
}
BOOST_AUTO_TEST_CASE (verify_text_not_too_early)
{
auto const dir = boost::filesystem::path("build/test/verify_text_not_too_early");
- prepare_directory (dir);
- auto asset = make_shared<dcp::SMPTESubtitleAsset>();
- asset->set_start_time (dcp::Time());
/* Just late enough */
- add_test_subtitle (asset, 4 * 24, 5 * 24);
- asset->set_language (dcp::LanguageTag("de-DE"));
- asset->write (dir / "subs.mxf");
-
- auto reel_asset = make_shared<dcp::ReelSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
- write_dcp_with_single_asset (dir, reel_asset);
-
+ dcp_with_subtitles (dir, { 4 * 24, 5 * 24 });
auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
- dump_notes (notes);
BOOST_REQUIRE (notes.empty());
}
/* Just late enough */
add_test_subtitle (asset1, 4 * 24, 5 * 24);
asset1->set_language (dcp::LanguageTag("de-DE"));
- asset1->write (dir / "subs.mxf");
+ asset1->write (dir / "subs1.mxf");
auto reel_asset1 = make_shared<dcp::ReelSubtitleAsset>(asset1, dcp::Fraction(24, 1), 16 * 24, 0);
auto reel1 = make_shared<dcp::Reel>();
reel1->add (reel_asset1);
/* This would be too early on first reel but should be OK on the second */
add_test_subtitle (asset2, 0, 4 * 24);
asset2->set_language (dcp::LanguageTag("de-DE"));
- asset2->write (dir / "subs.mxf");
+ asset2->write (dir / "subs2.mxf");
auto reel_asset2 = make_shared<dcp::ReelSubtitleAsset>(asset2, dcp::Fraction(24, 1), 16 * 24, 0);
auto reel2 = make_shared<dcp::Reel>();
reel2->add (reel_asset2);
dcp->write_xml (dcp::SMPTE);
auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
- dump_notes (notes);
BOOST_REQUIRE (notes.empty());
}
+
+
+BOOST_AUTO_TEST_CASE (verify_text_too_close)
+{
+ auto const dir = boost::filesystem::path("build/test/verify_text_too_close");
+ dcp_with_subtitles (
+ dir,
+ {
+ 4 * 24, 5 * 24,
+ 5 * 24 + 1, 6 * 24,
+ });
+ check_verify_result ({dir}, {{ dcp::VerificationNote::VERIFY_WARNING, dcp::VerificationNote::SUBTITLE_TOO_CLOSE }});
+}
+
+
+BOOST_AUTO_TEST_CASE (verify_text_not_too_close)
+{
+ auto const dir = boost::filesystem::path("build/test/verify_text_not_too_close");
+ dcp_with_subtitles (
+ dir,
+ {
+ 4 * 24, 5 * 24,
+ 5 * 24 + 16, 8 * 24,
+ });
+ auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
+ BOOST_REQUIRE (notes.empty());
+}
+
+
+BOOST_AUTO_TEST_CASE (verify_text_too_short)
+{
+ auto const dir = boost::filesystem::path("build/test/verify_text_too_short");
+ dcp_with_subtitles (
+ dir,
+ {
+ 4 * 24, 4 * 24 + 1,
+ });
+ check_verify_result ({dir}, {{ dcp::VerificationNote::VERIFY_WARNING, dcp::VerificationNote::SUBTITLE_TOO_SHORT }});
+}
+
+
+BOOST_AUTO_TEST_CASE (verify_text_not_too_short)
+{
+ auto const dir = boost::filesystem::path("build/test/verify_text_not_too_short");
+ dcp_with_subtitles (
+ dir,
+ {
+ 4 * 24, 4 * 24 + 17,
+ });
+ auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
+ BOOST_REQUIRE (notes.empty());
+}
+