Bv2.1 7.2.{6,7}: various limits on subtitle line and character counts.
authorCarl Hetherington <cth@carlh.net>
Tue, 12 Jan 2021 23:53:38 +0000 (00:53 +0100)
committerCarl Hetherington <cth@carlh.net>
Sun, 17 Jan 2021 19:13:23 +0000 (20:13 +0100)
src/verify.cc
src/verify.h
test/verify_test.cc

index a1d12e9bd65564f34265c10d49c2b63e214c2fd7..d1b4988a44ca18440f73b4a1c7214dfb7dfa16b2 100644 (file)
@@ -835,6 +835,105 @@ check_text_timing (
 }
 
 
+struct LinesCharactersResult
+{
+       bool warning_length_exceeded = false;
+       bool error_length_exceeded = false;
+       bool line_count_exceeded = false;
+};
+
+
+static
+void
+check_text_lines_and_characters (
+       shared_ptr<SubtitleAsset> asset,
+       int warning_length,
+       int error_length,
+       LinesCharactersResult* result
+       )
+{
+       class Event
+       {
+       public:
+               Event (dcp::Time time_, float position_, int characters_)
+                       : time (time_)
+                       , position (position_)
+                       , characters (characters_)
+               {}
+
+               Event (dcp::Time time_, shared_ptr<Event> start_)
+                       : time (time_)
+                       , start (start_)
+               {}
+
+               dcp::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
 check_text_timing (vector<shared_ptr<dcp::Reel>> reels, vector<VerificationNote>& notes)
@@ -980,6 +1079,38 @@ dcp::verify (
 
                        if (dcp->standard() == dcp::SMPTE) {
                                check_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);
+                                       }
+                               }
+
+                               if (result.line_count_exceeded) {
+                                       notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::TOO_MANY_SUBTITLE_LINES));
+                               }
+                               if (result.error_length_exceeded) {
+                                       notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::SUBTITLE_LINE_TOO_LONG));
+                               } else if (result.warning_length_exceeded) {
+                                       notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::SUBTITLE_LINE_LONGER_THAN_RECOMMENDED));
+                               }
+
+                               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);
+                                               }
+                                       }
+                               }
+
+                               if (result.line_count_exceeded) {
+                                       notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::TOO_MANY_CLOSED_CAPTION_LINES));
+                               }
+                               if (result.error_length_exceeded) {
+                                       notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::CLOSED_CAPTION_LINE_TOO_LONG));
+                               }
                        }
                }
 
@@ -1069,6 +1200,16 @@ dcp::note_to_string (dcp::VerificationNote note)
                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";
        }
 
        return "";
index 526b767ad9160d74331815266d9298a6f1b35f08..0398c909d365028b499ac50f5b69e6fb99d445bd 100644 (file)
@@ -121,6 +121,16 @@ public:
                SUBTITLE_TOO_SHORT,
                /** At least one pair of subtitles are separated by less than the the minimum of 2 frames suggested by [Bv2.1_7.2.5] */
                SUBTITLE_TOO_CLOSE,
+               /** There are more than 3 subtitle lines in at least one place [Bv2.1_7.2.7] */
+               TOO_MANY_SUBTITLE_LINES,
+               /** There are more than 52 characters in at least one subtitle line [Bv2.1_7.2.7] */
+               SUBTITLE_LINE_LONGER_THAN_RECOMMENDED,
+               /** There are more than 79 characters in at least one subtitle line [Bv2.1_7.2.7] */
+               SUBTITLE_LINE_TOO_LONG,
+               /** There are more than 3 closed caption lines in at least one place [Bv2.1_7.2.6] */
+               TOO_MANY_CLOSED_CAPTION_LINES,
+               /** There are more than 32 characters in at least one closed caption line [Bv2.1_7.2.6] */
+               CLOSED_CAPTION_LINE_TOO_LONG,
        };
 
        VerificationNote (Type type, Code code)
index 7e80f8996b79c99549718bcfca7f9e0281c01801..1877e284a6a99e6a5394e11fe5e28cdb20ab3dff 100644 (file)
@@ -1037,7 +1037,7 @@ BOOST_AUTO_TEST_CASE (verify_picture_size)
 
 static
 void
-add_test_subtitle (shared_ptr<dcp::SubtitleAsset> asset, int start_frame, int end_frame)
+add_test_subtitle (shared_ptr<dcp::SubtitleAsset> asset, int start_frame, int end_frame, float v_position = 0, string text = "Hello")
 {
        asset->add (
                make_shared<dcp::SubtitleString>(
@@ -1052,10 +1052,10 @@ add_test_subtitle (shared_ptr<dcp::SubtitleAsset> asset, int start_frame, int en
                        dcp::Time(end_frame, 24, 24),
                        0,
                        dcp::HALIGN_CENTER,
-                       0,
+                       v_position,
                        dcp::VALIGN_CENTER,
                        dcp::DIRECTION_LTR,
-                       "Hello",
+                       text,
                        dcp::NONE,
                        dcp::Colour(),
                        dcp::Time(),
@@ -1313,20 +1313,37 @@ BOOST_AUTO_TEST_CASE (verify_non_zero_start_time_tag_in_subtitle_xml)
 }
 
 
-static
+class TestText
+{
+public:
+       TestText (int in_, int out_, float v_position_ = 0, string text_ = "Hello")
+               : in(in_)
+               , out(out_)
+               , v_position(v_position_)
+               , text(text_)
+       {}
+
+       int in;
+       int out;
+       float v_position;
+       string text;
+};
+
+
+template <class T>
 void
-dcp_with_subtitles (boost::filesystem::path dir, vector<int> timings)
+dcp_with_text (boost::filesystem::path dir, vector<TestText> subs)
 {
        prepare_directory (dir);
        auto asset = make_shared<dcp::SMPTESubtitleAsset>();
        asset->set_start_time (dcp::Time());
-       for (auto i = 0U; i < timings.size(); i += 2) {
-               add_test_subtitle (asset, timings[i], timings[i + 1]);
+       for (auto i: subs) {
+               add_test_subtitle (asset, i.in, i.out, i.v_position, i.text);
        }
        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);
+       auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
        write_dcp_with_single_asset (dir, reel_asset);
 }
 
@@ -1335,7 +1352,7 @@ 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 });
+       dcp_with_text<dcp::ReelSubtitleAsset> (dir, {{ 4 * 24 - 1, 5 * 24 }});
        check_verify_result (
                { dir },
                {{ dcp::VerificationNote::VERIFY_WARNING, dcp::VerificationNote::FIRST_TEXT_TOO_EARLY }});
@@ -1346,7 +1363,7 @@ BOOST_AUTO_TEST_CASE (verify_text_not_too_early)
 {
        auto const dir = boost::filesystem::path("build/test/verify_text_not_too_early");
        /* Just late enough */
-       dcp_with_subtitles (dir, { 4 * 24, 5 * 24 });
+       dcp_with_text<dcp::ReelSubtitleAsset> (dir, {{ 4 * 24, 5 * 24 }});
        auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
        BOOST_REQUIRE (notes.empty());
 }
@@ -1392,11 +1409,11 @@ BOOST_AUTO_TEST_CASE (verify_text_early_on_second_reel)
 BOOST_AUTO_TEST_CASE (verify_text_too_close)
 {
        auto const dir = boost::filesystem::path("build/test/verify_text_too_close");
-       dcp_with_subtitles (
+       dcp_with_text<dcp::ReelSubtitleAsset> (
                dir,
                {
-                 4 * 24,     5 * 24,
-                 5 * 24 + 1, 6 * 24,
+                       { 4 * 24,     5 * 24 },
+                       { 5 * 24 + 1, 6 * 24 },
                });
        check_verify_result ({dir}, {{ dcp::VerificationNote::VERIFY_WARNING, dcp::VerificationNote::SUBTITLE_TOO_CLOSE }});
 }
@@ -1405,11 +1422,11 @@ BOOST_AUTO_TEST_CASE (verify_text_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 (
+       dcp_with_text<dcp::ReelSubtitleAsset> (
                dir,
                {
-                 4 * 24,     5 * 24,
-                 5 * 24 + 16, 8 * 24,
+                       { 4 * 24,      5 * 24 },
+                       { 5 * 24 + 16, 8 * 24 },
                });
        auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
        BOOST_REQUIRE (notes.empty());
@@ -1419,11 +1436,7 @@ BOOST_AUTO_TEST_CASE (verify_text_not_too_close)
 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,
-               });
+       dcp_with_text<dcp::ReelSubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 1 }});
        check_verify_result ({dir}, {{ dcp::VerificationNote::VERIFY_WARNING, dcp::VerificationNote::SUBTITLE_TOO_SHORT }});
 }
 
@@ -1431,12 +1444,166 @@ BOOST_AUTO_TEST_CASE (verify_text_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 (
+       dcp_with_text<dcp::ReelSubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 17 }});
+       auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
+       BOOST_REQUIRE (notes.empty());
+}
+
+
+BOOST_AUTO_TEST_CASE (verify_too_many_subtitle_lines1)
+{
+       auto const dir = boost::filesystem::path ("verify_too_many_subtitle_lines1");
+       dcp_with_text<dcp::ReelSubtitleAsset> (
+               dir,
+               {
+                       { 96, 200, 0.0, "We" },
+                       { 96, 200, 0.1, "have" },
+                       { 96, 200, 0.2, "four" },
+                       { 96, 200, 0.3, "lines" }
+               });
+       check_verify_result ({dir}, {{ dcp::VerificationNote::VERIFY_WARNING, dcp::VerificationNote::TOO_MANY_SUBTITLE_LINES}});
+}
+
+
+BOOST_AUTO_TEST_CASE (verify_not_too_many_subtitle_lines1)
+{
+       auto const dir = boost::filesystem::path ("verify_not_too_many_subtitle_lines1");
+       dcp_with_text<dcp::ReelSubtitleAsset> (
+               dir,
+               {
+                       { 96, 200, 0.0, "We" },
+                       { 96, 200, 0.1, "have" },
+                       { 96, 200, 0.2, "four" },
+               });
+       auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
+       BOOST_REQUIRE (notes.empty());
+}
+
+
+BOOST_AUTO_TEST_CASE (verify_too_many_subtitle_lines2)
+{
+       auto const dir = boost::filesystem::path ("verify_too_many_subtitle_lines2");
+       dcp_with_text<dcp::ReelSubtitleAsset> (
+               dir,
+               {
+                       { 96, 300, 0.0, "We" },
+                       { 96, 300, 0.1, "have" },
+                       { 150, 180, 0.2, "four" },
+                       { 150, 180, 0.3, "lines" }
+               });
+       check_verify_result ({dir}, {{ dcp::VerificationNote::VERIFY_WARNING, dcp::VerificationNote::TOO_MANY_SUBTITLE_LINES}});
+}
+
+
+BOOST_AUTO_TEST_CASE (verify_not_too_many_subtitle_lines2)
+{
+       auto const dir = boost::filesystem::path ("verify_not_too_many_subtitle_lines2");
+       dcp_with_text<dcp::ReelSubtitleAsset> (
                dir,
                {
-                 4 * 24,     4 * 24 + 17,
+                       { 96, 300, 0.0, "We" },
+                       { 96, 300, 0.1, "have" },
+                       { 150, 180, 0.2, "four" },
+                       { 190, 250, 0.3, "lines" }
                });
        auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
        BOOST_REQUIRE (notes.empty());
 }
 
+
+BOOST_AUTO_TEST_CASE (verify_subtitle_lines_too_long1)
+{
+       auto const dir = boost::filesystem::path ("verify_subtitle_lines_too_long1");
+       dcp_with_text<dcp::ReelSubtitleAsset> (
+               dir,
+               {
+                       { 96, 300, 0.0, "012345678901234567890123456789012345678901234567890123" }
+               });
+       check_verify_result ({dir}, {{ dcp::VerificationNote::VERIFY_WARNING, dcp::VerificationNote::SUBTITLE_LINE_LONGER_THAN_RECOMMENDED }});
+}
+
+
+BOOST_AUTO_TEST_CASE (verify_subtitle_lines_too_long2)
+{
+       auto const dir = boost::filesystem::path ("verify_subtitle_lines_too_long2");
+       dcp_with_text<dcp::ReelSubtitleAsset> (
+               dir,
+               {
+                       { 96, 300, 0.0, "012345678901234567890123456789012345678901234567890123456789012345678901234567890" }
+               });
+       check_verify_result ({dir}, {{ dcp::VerificationNote::VERIFY_WARNING, dcp::VerificationNote::SUBTITLE_LINE_TOO_LONG }});
+}
+
+
+BOOST_AUTO_TEST_CASE (verify_too_many_closed_caption_lines1)
+{
+       auto const dir = boost::filesystem::path ("verify_too_many_closed_caption_lines1");
+       dcp_with_text<dcp::ReelClosedCaptionAsset> (
+               dir,
+               {
+                       { 96, 200, 0.0, "We" },
+                       { 96, 200, 0.1, "have" },
+                       { 96, 200, 0.2, "four" },
+                       { 96, 200, 0.3, "lines" }
+               });
+       check_verify_result ({dir}, {{ dcp::VerificationNote::VERIFY_BV21_ERROR, dcp::VerificationNote::TOO_MANY_CLOSED_CAPTION_LINES}});
+}
+
+
+BOOST_AUTO_TEST_CASE (verify_not_too_many_closed_caption_lines1)
+{
+       auto const dir = boost::filesystem::path ("verify_not_too_many_closed_caption_lines1");
+       dcp_with_text<dcp::ReelClosedCaptionAsset> (
+               dir,
+               {
+                       { 96, 200, 0.0, "We" },
+                       { 96, 200, 0.1, "have" },
+                       { 96, 200, 0.2, "four" },
+               });
+       auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
+       BOOST_REQUIRE (notes.empty());
+}
+
+
+BOOST_AUTO_TEST_CASE (verify_too_many_closed_caption_lines2)
+{
+       auto const dir = boost::filesystem::path ("verify_too_many_closed_caption_lines2");
+       dcp_with_text<dcp::ReelClosedCaptionAsset> (
+               dir,
+               {
+                       { 96, 300, 0.0, "We" },
+                       { 96, 300, 0.1, "have" },
+                       { 150, 180, 0.2, "four" },
+                       { 150, 180, 0.3, "lines" }
+               });
+       check_verify_result ({dir}, {{ dcp::VerificationNote::VERIFY_BV21_ERROR, dcp::VerificationNote::TOO_MANY_CLOSED_CAPTION_LINES}});
+}
+
+
+BOOST_AUTO_TEST_CASE (verify_not_too_many_closed_caption_lines2)
+{
+       auto const dir = boost::filesystem::path ("verify_not_too_many_closed_caption_lines2");
+       dcp_with_text<dcp::ReelClosedCaptionAsset> (
+               dir,
+               {
+                       { 96, 300, 0.0, "We" },
+                       { 96, 300, 0.1, "have" },
+                       { 150, 180, 0.2, "four" },
+                       { 190, 250, 0.3, "lines" }
+               });
+       auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
+       BOOST_REQUIRE (notes.empty());
+}
+
+
+BOOST_AUTO_TEST_CASE (verify_closed_caption_lines_too_long1)
+{
+       auto const dir = boost::filesystem::path ("verify_closed_caption_lines_too_long1");
+       dcp_with_text<dcp::ReelClosedCaptionAsset> (
+               dir,
+               {
+                       { 96, 300, 0.0, "0123456789012345678901234567890123" }
+               });
+       check_verify_result ({dir}, {{ dcp::VerificationNote::VERIFY_BV21_ERROR, dcp::VerificationNote::CLOSED_CAPTION_LINE_TOO_LONG }});
+}
+