Order subtitles in the XML according to their vertical position (DoM bug #2106). v1.8.4
authorCarl Hetherington <cth@carlh.net>
Mon, 18 Oct 2021 06:55:27 +0000 (08:55 +0200)
committerCarl Hetherington <cth@carlh.net>
Sun, 24 Oct 2021 18:52:11 +0000 (20:52 +0200)
src/smpte_subtitle_asset.h
src/subtitle_asset.cc
src/verify.cc
src/verify.h
test/data/verify_incorrect_closed_caption_ordering3.xml [new file with mode: 0644]
test/data/verify_incorrect_closed_caption_ordering4.xml [new file with mode: 0644]
test/smpte_subtitle_test.cc
test/verify_test.cc

index 4e220d4d586003a06da83ab79ec60fc3afc1d409..b707da124ba980f605345266cc3ec2939f9c744a 100644 (file)
@@ -58,6 +58,8 @@ namespace ASDCP {
 
 struct verify_invalid_language1;
 struct verify_invalid_language2;
+struct write_subtitles_in_vertical_order_with_top_alignment;
+struct write_subtitles_in_vertical_order_with_bottom_alignment;
 
 
 namespace dcp {
@@ -204,6 +206,8 @@ private:
        friend struct ::write_smpte_subtitle_test2;
        friend struct ::verify_invalid_language1;
        friend struct ::verify_invalid_language2;
+       friend struct ::write_subtitles_in_vertical_order_with_top_alignment;
+       friend struct ::write_subtitles_in_vertical_order_with_bottom_alignment;
 
        void read_fonts (std::shared_ptr<ASDCP::TimedText::MXFReader>);
        void parse_xml (std::shared_ptr<cxml::Document> xml);
index 2793772a8e949fd7a8d384d73d0d4c3a8bc85aba..22781196ac5c485ca042ba8813895e06a85db7f1 100644 (file)
@@ -579,6 +579,9 @@ struct SubtitleSorter
                if (a->in() != b->in()) {
                        return a->in() < b->in();
                }
+               if (a->v_align() == VAlign::BOTTOM) {
+                       return a->v_position() > b->v_position();
+               }
                return a->v_position() < b->v_position();
        }
 };
index 771898da5b8869e9f5a85230ac62738b27709004..8a065e689756410877435bb71969da887265a07d 100644 (file)
@@ -921,6 +921,113 @@ verify_text_details (
 }
 
 
+static
+void
+verify_closed_caption_details (
+       vector<shared_ptr<Reel>> reels,
+       vector<VerificationNote>& notes
+       )
+{
+       std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
+       find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
+               for (auto i: node->node_children()) {
+                       if (i->name() == "Text") {
+                               text_or_image.push_back (i);
+                       } else {
+                               find_text_or_image (i, text_or_image);
+                       }
+               }
+       };
+
+       auto mismatched_valign = false;
+       auto incorrect_order = false;
+
+       std::function<void (cxml::ConstNodePtr)> parse;
+       parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
+               if (node->name() == "Subtitle") {
+                       vector<cxml::ConstNodePtr> text_or_image;
+                       find_text_or_image (node, text_or_image);
+                       optional<string> last_valign;
+                       optional<float> last_vpos;
+                       for (auto i: text_or_image) {
+                               auto valign = i->optional_string_attribute("VAlign");
+                               if (!valign) {
+                                       valign = i->optional_string_attribute("Valign").get_value_or("center");
+                               }
+                               auto vpos = i->optional_number_attribute<float>("VPosition");
+                               if (!vpos) {
+                                       vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
+                               }
+
+                               if (last_valign) {
+                                       if (*last_valign != valign) {
+                                               mismatched_valign = true;
+                                       }
+                               }
+                               last_valign = valign;
+
+                               if (!mismatched_valign) {
+                                       if (last_vpos) {
+                                               if (*last_valign == "top" || *last_valign == "center") {
+                                                       if (*vpos < *last_vpos) {
+                                                               incorrect_order = true;
+                                                       }
+                                               } else {
+                                                       if (*vpos > *last_vpos) {
+                                                               incorrect_order = true;
+                                                       }
+                                               }
+                                       }
+                                       last_vpos = vpos;
+                               }
+                       }
+               }
+
+               for (auto i: node->node_children()) {
+                       parse(i);
+               }
+       };
+
+       for (auto reel: reels) {
+               for (auto ccap: reel->closed_captions()) {
+                       auto reel_xml = ccap->asset()->raw_xml();
+                       if (!reel_xml) {
+                               notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
+                               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.
+                        */
+
+                       shared_ptr<cxml::Document> doc;
+                       optional<int> tcr;
+                       optional<Time> start_time;
+                       try {
+                               doc = make_shared<cxml::Document>("SubtitleReel");
+                               doc->read_string (*reel_xml);
+                       } catch (...) {
+                               doc = make_shared<cxml::Document>("DCSubtitle");
+                               doc->read_string (*reel_xml);
+                       }
+                       parse (doc);
+               }
+       }
+
+       if (mismatched_valign) {
+               notes.push_back ({
+                       VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN,
+               });
+       }
+
+       if (incorrect_order) {
+               notes.push_back ({
+                       VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING,
+               });
+       }
+}
+
+
 struct LinesCharactersResult
 {
        bool warning_length_exceeded = false;
@@ -1061,6 +1168,8 @@ verify_text_details (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& n
                        }
                );
        }
+
+       verify_closed_caption_details (reels, notes);
 }
 
 
@@ -1650,6 +1759,10 @@ dcp::note_to_string (VerificationNote note)
                return "Some aspect of this DCP could not be checked because it is encrypted.";
        case VerificationNote::Code::EMPTY_TEXT:
                return "There is an empty <Text> node in a subtitle or closed caption.";
+       case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
+               return "Some closed <Text> or <Image> nodes have different vertical alignment within a <Subtitle>.";
+       case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
+               return "Some closed captions are not listed in the order of their vertical position.";
        }
 
        return "";
index c10a0e7ea872f4e404044be68b8306e4ed908628..424b29e7ca983387800116931f391fa2b301427a 100644 (file)
@@ -387,7 +387,11 @@ public:
                /** Something could not be verified because content is encrypted and no key is available */
                MISSED_CHECK_OF_ENCRYPTED,
                /** Some timed-text XML has an empty <_Text_> node */
-               EMPTY_TEXT
+               EMPTY_TEXT,
+               /** Some closed captions do not have the same vertical alignment within a <_Subtitle_> node */
+               MISMATCHED_CLOSED_CAPTION_VALIGN,
+               /** Some closed captions are not listed in the XML in the order of their vertical position */
+               INCORRECT_CLOSED_CAPTION_ORDERING,
        };
 
        VerificationNote (Type type, Code code)
diff --git a/test/data/verify_incorrect_closed_caption_ordering3.xml b/test/data/verify_incorrect_closed_caption_ordering3.xml
new file mode 100644 (file)
index 0000000..c6dffa2
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SubtitleReel xmlns="http://www.smpte-ra.org/schemas/428-7/2010/DCST" xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <Id>urn:uuid:fc754ec1-eb8d-4029-8023-80d7dadc6313</Id>
+  <ContentTitleText></ContentTitleText>
+  <IssueDate>2021-10-24T00:23:49.000+02:00</IssueDate>
+  <Language>de-DE</Language>
+  <EditRate>24 1</EditRate>
+  <TimeCodeRate>24</TimeCodeRate>
+  <StartTime>00:00:00:00</StartTime>
+  <SubtitleList>
+    <Font AspectAdjust="1.0" Color="FF000000" Effect="none" EffectColor="FF000000" Italic="no" Script="normal" Size="42" Underline="no" Weight="normal">
+      <Subtitle FadeDownTime="00:00:00:00" FadeUpTime="00:00:00:00" SpotNumber="1" TimeIn="00:00:04:00" TimeOut="00:00:12:12">
+        <Text Valign="center" Vposition="20">This</Text>
+        <Text Valign="center" Vposition="10">is</Text>
+        <Text Valign="center" Vposition="0">also not fine</Text>
+      </Subtitle>
+    </Font>
+  </SubtitleList>
+</SubtitleReel>
+
diff --git a/test/data/verify_incorrect_closed_caption_ordering4.xml b/test/data/verify_incorrect_closed_caption_ordering4.xml
new file mode 100644 (file)
index 0000000..5fc2b49
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SubtitleReel xmlns="http://www.smpte-ra.org/schemas/428-7/2010/DCST" xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <Id>urn:uuid:fc754ec1-eb8d-4029-8023-80d7dadc6313</Id>
+  <ContentTitleText></ContentTitleText>
+  <IssueDate>2021-10-24T00:23:49.000+02:00</IssueDate>
+  <Language>de-DE</Language>
+  <EditRate>24 1</EditRate>
+  <TimeCodeRate>24</TimeCodeRate>
+  <StartTime>00:00:00:00</StartTime>
+  <SubtitleList>
+    <Font AspectAdjust="1.0" Color="FF000000" Effect="none" EffectColor="FF000000" Italic="no" Script="normal" Size="42" Underline="no" Weight="normal">
+      <Subtitle FadeDownTime="00:00:00:00" FadeUpTime="00:00:00:00" SpotNumber="1" TimeIn="00:00:04:00" TimeOut="00:00:12:12">
+        <Text Valign="bottom" Vposition="20">This</Text>
+        <Text Valign="bottom" Vposition="10">is</Text>
+        <Text Valign="bottom" Vposition="0">fine, though.</Text>
+      </Subtitle>
+    </Font>
+  </SubtitleList>
+</SubtitleReel>
+
index b1ad405836507528100205a0978d1751612ff684..2b8491a35443b36ae9a005210dbca94b965b2c12 100644 (file)
@@ -526,3 +526,178 @@ BOOST_AUTO_TEST_CASE (write_smpte_subtitle_test3)
        BOOST_CHECK (image->fade_up_time() == dcp::Time(0, 0, 0, 0, 24));
        BOOST_CHECK (image->fade_down_time() == dcp::Time(0, 0, 0, 0, 24));
 }
+
+
+/* Some closed caption systems require the <Text> elements to be written in order of their
+ * vertical position (see DoM bug #2106).
+ */
+BOOST_AUTO_TEST_CASE (write_subtitles_in_vertical_order_with_top_alignment)
+{
+       dcp::SMPTESubtitleAsset c;
+       c.set_reel_number (1);
+       c.set_language (dcp::LanguageTag("en"));
+       c.set_content_title_text ("Test");
+       c.set_issue_date (dcp::LocalTime ("2016-04-01T03:52:00+00:00"));
+
+       c.add (
+               make_shared<dcp::SubtitleString>(
+                       string ("Arial"),
+                       false,
+                       false,
+                       false,
+                       dcp::Colour (255, 255, 255),
+                       48,
+                       1.0,
+                       dcp::Time (0, 0, 1, 0, 24),
+                       dcp::Time (0, 0, 9, 0, 24),
+                       0,
+                       dcp::HAlign::CENTER,
+                       0.8,
+                       dcp::VAlign::TOP,
+                       dcp::Direction::LTR,
+                       "Top line",
+                       dcp::Effect::NONE,
+                       dcp::Colour (0, 0, 0),
+                       dcp::Time (0, 0, 0, 0, 24),
+                       dcp::Time (0, 0, 0, 0, 24),
+                       0
+                       )
+               );
+
+       c.add (
+               make_shared<dcp::SubtitleString>(
+                       string ("Arial"),
+                       false,
+                       false,
+                       false,
+                       dcp::Colour (255, 255, 255),
+                       48,
+                       1.0,
+                       dcp::Time (0, 0, 1, 0, 24),
+                       dcp::Time (0, 0, 9, 0, 24),
+                       0,
+                       dcp::HAlign::CENTER,
+                       0.9,
+                       dcp::VAlign::TOP,
+                       dcp::Direction::LTR,
+                       "Bottom line",
+                       dcp::Effect::NONE,
+                       dcp::Colour (0, 0, 0),
+                       dcp::Time (0, 0, 0, 0, 24),
+                       dcp::Time (0, 0, 0, 0, 24),
+                       0
+                       )
+               );
+
+       c._xml_id = "a6c58cff-3e1e-4b38-acec-a42224475ef6";
+
+       check_xml (
+               c.xml_as_string(),
+               "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+               "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">"
+                 "<Id>urn:uuid:a6c58cff-3e1e-4b38-acec-a42224475ef6</Id>"
+                 "<ContentTitleText>Test</ContentTitleText>"
+                 "<IssueDate>2016-04-01T03:52:00.000+00:00</IssueDate>"
+                 "<ReelNumber>1</ReelNumber>"
+                 "<Language>en</Language>"
+                 "<EditRate>24 1</EditRate>"
+                 "<TimeCodeRate>24</TimeCodeRate>"
+                 "<SubtitleList>"
+                   "<Font AspectAdjust=\"1.0\" Color=\"FFFFFFFF\" Effect=\"none\" EffectColor=\"FF000000\" ID=\"Arial\" Italic=\"no\" Script=\"normal\" Size=\"48\" Underline=\"no\" Weight=\"normal\">"
+                     "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:01:00\" TimeOut=\"00:00:09:00\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
+                       "<Text Valign=\"top\" Vposition=\"80\">Top line</Text>"
+                       "<Text Valign=\"top\" Vposition=\"90\">Bottom line</Text>"
+                     "</Subtitle>"
+                   "</Font>"
+                 "</SubtitleList>"
+               "</SubtitleReel>",
+               {}
+               );
+}
+
+
+/* See the test above */
+BOOST_AUTO_TEST_CASE (write_subtitles_in_vertical_order_with_bottom_alignment)
+{
+       dcp::SMPTESubtitleAsset c;
+       c.set_reel_number (1);
+       c.set_language (dcp::LanguageTag("en"));
+       c.set_content_title_text ("Test");
+       c.set_issue_date (dcp::LocalTime ("2016-04-01T03:52:00+00:00"));
+
+       c.add (
+               make_shared<dcp::SubtitleString>(
+                       string ("Arial"),
+                       false,
+                       false,
+                       false,
+                       dcp::Colour (255, 255, 255),
+                       48,
+                       1.0,
+                       dcp::Time (0, 0, 1, 0, 24),
+                       dcp::Time (0, 0, 9, 0, 24),
+                       0,
+                       dcp::HAlign::CENTER,
+                       0.8,
+                       dcp::VAlign::BOTTOM,
+                       dcp::Direction::LTR,
+                       "Top line",
+                       dcp::Effect::NONE,
+                       dcp::Colour (0, 0, 0),
+                       dcp::Time (0, 0, 0, 0, 24),
+                       dcp::Time (0, 0, 0, 0, 24),
+                       0
+                       )
+               );
+
+       c.add (
+               make_shared<dcp::SubtitleString>(
+                       string ("Arial"),
+                       false,
+                       false,
+                       false,
+                       dcp::Colour (255, 255, 255),
+                       48,
+                       1.0,
+                       dcp::Time (0, 0, 1, 0, 24),
+                       dcp::Time (0, 0, 9, 0, 24),
+                       0,
+                       dcp::HAlign::CENTER,
+                       0.7,
+                       dcp::VAlign::BOTTOM,
+                       dcp::Direction::LTR,
+                       "Bottom line",
+                       dcp::Effect::NONE,
+                       dcp::Colour (0, 0, 0),
+                       dcp::Time (0, 0, 0, 0, 24),
+                       dcp::Time (0, 0, 0, 0, 24),
+                       0
+                       )
+               );
+
+       c._xml_id = "a6c58cff-3e1e-4b38-acec-a42224475ef6";
+
+       check_xml (
+               c.xml_as_string(),
+               "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+               "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">"
+                 "<Id>urn:uuid:a6c58cff-3e1e-4b38-acec-a42224475ef6</Id>"
+                 "<ContentTitleText>Test</ContentTitleText>"
+                 "<IssueDate>2016-04-01T03:52:00.000+00:00</IssueDate>"
+                 "<ReelNumber>1</ReelNumber>"
+                 "<Language>en</Language>"
+                 "<EditRate>24 1</EditRate>"
+                 "<TimeCodeRate>24</TimeCodeRate>"
+                 "<SubtitleList>"
+                   "<Font AspectAdjust=\"1.0\" Color=\"FFFFFFFF\" Effect=\"none\" EffectColor=\"FF000000\" ID=\"Arial\" Italic=\"no\" Script=\"normal\" Size=\"48\" Underline=\"no\" Weight=\"normal\">"
+                     "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:01:00\" TimeOut=\"00:00:09:00\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
+                       "<Text Valign=\"bottom\" Vposition=\"80\">Top line</Text>"
+                       "<Text Valign=\"bottom\" Vposition=\"70\">Bottom line</Text>"
+                     "</Subtitle>"
+                   "</Font>"
+                 "</SubtitleList>"
+               "</SubtitleReel>",
+               {}
+               );
+}
+
index ab0a2f26e339d9f8e61b2651ace45293054a9575..c8d0e579046db80084384a3aedb01a45b3d48e8d 100644 (file)
@@ -1618,6 +1618,43 @@ dcp_with_text (path dir, vector<TestText> subs)
 }
 
 
+template <class T>
+shared_ptr<dcp::CPL>
+dcp_with_text_from_file (path dir, boost::filesystem::path subs_xml)
+{
+       prepare_directory (dir);
+       auto asset = make_shared<dcp::SMPTESubtitleAsset>(subs_xml);
+       asset->set_start_time (dcp::Time());
+       asset->set_language (dcp::LanguageTag("de-DE"));
+
+       auto subs_mxf = dir / "subs.mxf";
+       asset->write (subs_mxf);
+
+       /* The call to write() puts the asset into the DCP correctly but it will have
+        * XML re-written by our parser.  Overwrite the MXF using the given file's verbatim
+        * contents.
+        */
+       ASDCP::TimedText::MXFWriter writer;
+       ASDCP::WriterInfo writer_info;
+       writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
+       unsigned int c;
+       Kumu::hex2bin (asset->id().c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
+       DCP_ASSERT (c == Kumu::UUID_Length);
+       ASDCP::TimedText::TimedTextDescriptor descriptor;
+       descriptor.ContainerDuration = asset->intrinsic_duration();
+       Kumu::hex2bin (asset->xml_id()->c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
+       DCP_ASSERT (c == Kumu::UUID_Length);
+       ASDCP::Result_t r = writer.OpenWrite (subs_mxf.string().c_str(), writer_info, descriptor, 16384);
+       BOOST_REQUIRE (!ASDCP_FAILURE(r));
+       r = writer.WriteTimedTextResource (dcp::file_to_string(subs_xml));
+       BOOST_REQUIRE (!ASDCP_FAILURE(r));
+       writer.Finalize ();
+
+       auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), asset->intrinsic_duration(), 0);
+       return write_dcp_with_single_asset (dir, reel_asset);
+}
+
+
 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_first_text_time)
 {
        auto const dir = path("build/test/verify_invalid_subtitle_first_text_time");
@@ -1955,6 +1992,105 @@ BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_length)
 }
 
 
+BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_valign1)
+{
+       auto const dir = path ("build/test/verify_mismatched_closed_caption_valign1");
+       auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
+               dir,
+               {
+                       { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
+                       { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
+                       { 96, 300, 0.2, dcp::VAlign::TOP, "fine" },
+               });
+       check_verify_result (
+               {dir},
+               {
+                       { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
+               });
+}
+
+
+BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_valign2)
+{
+       auto const dir = path ("build/test/verify_mismatched_closed_caption_valign2");
+       auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
+               dir,
+               {
+                       { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
+                       { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
+                       { 96, 300, 0.2, dcp::VAlign::CENTER, "not fine" },
+               });
+       check_verify_result (
+               {dir},
+               {
+                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN },
+                       { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
+               });
+}
+
+
+BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering1)
+{
+       auto const dir = path ("build/test/verify_invalid_incorrect_closed_caption_ordering1");
+       auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
+               dir,
+               {
+                       { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
+                       { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
+                       { 96, 300, 0.2, dcp::VAlign::TOP, "fine" },
+               });
+       check_verify_result (
+               {dir},
+               {
+                       { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
+               });
+}
+
+
+BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering2)
+{
+       auto const dir = path ("build/test/verify_invalid_incorrect_closed_caption_ordering2");
+       auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
+               dir,
+               {
+                       { 96, 300, 0.2, dcp::VAlign::BOTTOM, "This" },
+                       { 96, 300, 0.1, dcp::VAlign::BOTTOM, "is" },
+                       { 96, 300, 0.0, dcp::VAlign::BOTTOM, "also fine" },
+               });
+       check_verify_result (
+               {dir},
+               {
+                       { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
+               });
+}
+
+
+BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering3)
+{
+       auto const dir = path ("build/test/verify_incorrect_closed_caption_ordering3");
+       auto cpl = dcp_with_text_from_file<dcp::ReelSMPTEClosedCaptionAsset> (dir, "test/data/verify_incorrect_closed_caption_ordering3.xml");
+       check_verify_result (
+               {dir},
+               {
+                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING },
+                       { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
+               });
+}
+
+
+BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering4)
+{
+       auto const dir = path ("build/test/verify_incorrect_closed_caption_ordering4");
+       auto cpl = dcp_with_text_from_file<dcp::ReelSMPTEClosedCaptionAsset> (dir, "test/data/verify_incorrect_closed_caption_ordering4.xml");
+       check_verify_result (
+               {dir},
+               {
+                       { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
+               });
+}
+
+
+
 BOOST_AUTO_TEST_CASE (verify_invalid_sound_frame_rate)
 {
        path const dir("build/test/verify_invalid_sound_frame_rate");