}
+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)
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));
+ }
}
}
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 "";
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>(
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(),
}
-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);
}
{
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 }});
{
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());
}
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 }});
}
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());
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 }});
}
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 }});
+}
+