Fix various bugs in subtitle/ccap verification.
[libdcp.git] / src / verify.cc
index bddf1683cc1c8b926fcfdf216da2d212f21933ae..97758e6192e632aae3bf4c498c167a90120818ad 100644 (file)
@@ -763,7 +763,7 @@ static
 void
 verify_text_timing (
        vector<shared_ptr<Reel>> reels,
-       optional<int> picture_frame_rate,
+       int edit_rate,
        vector<VerificationNote>& notes,
        std::function<bool (shared_ptr<Reel>)> check,
        std::function<string (shared_ptr<Reel>)> xml,
@@ -775,32 +775,39 @@ verify_text_timing (
        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, 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) {
+       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 (first_reel && in < Time(0, 0, 4, 0, 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(pfr) < 15) {
+                       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(pfr) - *last_out;
+                               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(pfr);
+                       last_out = reel_offset + out.as_editable_units_floor(er);
                } else {
                        for (auto i: node->node_children()) {
-                               parse(i, tcr, pfr, first_reel);
+                               parse(i, tcr, start_time, er, first_reel);
                        }
                }
        };
@@ -814,11 +821,31 @@ verify_text_timing (
                 * 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]);
+               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) {
@@ -838,6 +865,12 @@ verify_text_timing (
                        VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
                });
        }
+
+       if (reel_overlap) {
+               notes.push_back ({
+                       VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
+               });
+       }
 }
 
 
@@ -948,13 +981,8 @@ verify_text_timing (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& no
                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()) {
-               verify_text_timing (reels, picture_frame_rate, notes,
+               verify_text_timing (reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
                        [](shared_ptr<Reel> reel) {
                                return static_cast<bool>(reel->main_subtitle());
                        },
@@ -968,7 +996,7 @@ verify_text_timing (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& no
        }
 
        for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
-               verify_text_timing (reels, picture_frame_rate, notes,
+               verify_text_timing (reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
                        [i](shared_ptr<Reel> reel) {
                                return i < reel->closed_captions().size();
                        },
@@ -1262,6 +1290,8 @@ dcp::verify (
                                most_closed_captions = std::max (most_closed_captions, reel->closed_captions().size());
                        }
 
+                       verify_text_timing (cpl->reels(), notes);
+
                        if (dcp->standard() == Standard::SMPTE) {
 
                                if (have_main_subtitle && have_no_main_subtitle) {
@@ -1292,14 +1322,12 @@ dcp::verify (
                                if (lfoc == markers_seen.end()) {
                                        notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
                                } else {
-                                       auto lfoc_time = lfoc->second.as_editable_units(lfoc->second.tcr);
+                                       auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
                                        if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
                                                notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
                                        }
                                }
 
-                               verify_text_timing (cpl->reels(), notes);
-
                                LinesCharactersResult result;
                                for (auto reel: cpl->reels()) {
                                        if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
@@ -1458,6 +1486,8 @@ dcp::note_to_string (VerificationNote note)
                return "At least one subtitle lasts less than 15 frames.";
        case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
                return "At least one pair of subtitles is separated by less than 2 frames.";
+       case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
+               return "At least one subtitle extends outside of its reel.";
        case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
                return "There are more than 3 subtitle lines in at least one place in the DCP.";
        case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH: