+ validate_xml (asset->raw_xml(), xsd_dtd_directory, notes);
+
+ auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
+ if (smpte) {
+ verify_smpte_subtitle_asset (smpte, notes, state);
+ }
+}
+
+
+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,
+ State& state
+ )
+{
+ verify_subtitle_asset (asset, stage, xsd_dtd_directory, notes, state);
+
+ 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,
+ optional<int> picture_frame_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;
+ /* 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") {
+ Time in (node->string_attribute("TimeIn"), tcr);
+ Time out (node->string_attribute("TimeOut"), tcr);
+ if (first_reel && in < 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) {
+ 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.
+ */
+
+ 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::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
+ });
+ }
+}
+
+
+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;
+ }
+
+ 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,
+ [](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, picture_frame_rate, 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();
+ }
+ );
+ }