Order subtitles in the XML according to their vertical position (DoM bug #2106).
[libdcp.git] / test / verify_test.cc
index bfa52b0a5d386e96b8ef01db9bfa80e4d743d55b..c8d0e579046db80084384a3aedb01a45b3d48e8d 100644 (file)
 #include "openjpeg_image.h"
 #include "raw_convert.h"
 #include "reel.h"
-#include "reel_closed_caption_asset.h"
+#include "reel_interop_closed_caption_asset.h"
 #include "reel_interop_subtitle_asset.h"
 #include "reel_markers_asset.h"
 #include "reel_mono_picture_asset.h"
 #include "reel_sound_asset.h"
 #include "reel_stereo_picture_asset.h"
+#include "reel_smpte_closed_caption_asset.h"
 #include "reel_smpte_subtitle_asset.h"
 #include "smpte_subtitle_asset.h"
 #include "stereo_picture_asset.h"
@@ -60,6 +61,7 @@
 #include <cstdio>
 #include <iostream>
 
+
 using std::list;
 using std::pair;
 using std::string;
@@ -72,13 +74,25 @@ using std::shared_ptr;
 
 
 static list<pair<string, optional<path>>> stages;
-static string const dcp_test1_pkl_id = "6af1e0c1-c441-47f8-a502-3efd46b1fa4f";
-static string const dcp_test1_pkl = "pkl_" + dcp_test1_pkl_id + ".xml";
-static string const dcp_test1_cpl_id = "81fb54df-e1bf-4647-8788-ea7ba154375b";
-static string const dcp_test1_cpl = "cpl_" + dcp_test1_cpl_id + ".xml";
+
+static string filename_to_id(boost::filesystem::path path)
+{
+       return path.string().substr(4, path.string().length() - 8);
+}
+
+static boost::filesystem::path const dcp_test1_pkl = find_file("test/ref/DCP/dcp_test1", "pkl_").filename();
+static string const dcp_test1_pkl_id = filename_to_id(dcp_test1_pkl);
+
+static boost::filesystem::path const dcp_test1_cpl = find_file("test/ref/DCP/dcp_test1", "cpl_").filename();
+static string const dcp_test1_cpl_id = filename_to_id(dcp_test1_cpl);
+
 static string const dcp_test1_asset_map_id = "5d51e8a1-b2a5-4da6-9b66-4615c3609440";
-static string const encryption_test_cpl_id = "81fb54df-e1bf-4647-8788-ea7ba154375b";
-static string const encryption_test_pkl_id = "627ad740-ae36-4c49-92bb-553a9f09c4f8";
+
+static boost::filesystem::path const encryption_test_cpl = find_file("test/ref/DCP/encryption_test", "cpl_").filename();
+static string const encryption_test_cpl_id = filename_to_id(encryption_test_cpl);
+
+static boost::filesystem::path const encryption_test_pkl = find_file("test/ref/DCP/encryption_test", "pkl_").filename();
+static string const encryption_test_pkl_id = filename_to_id(encryption_test_pkl);
 
 static void
 stage (string s, optional<path> p)
@@ -164,6 +178,23 @@ public:
                BOOST_REQUIRE (_content != old_content);
        }
 
+       void delete_first_line_containing (string s)
+       {
+               vector<string> lines;
+               boost::algorithm::split (lines, _content, boost::is_any_of("\r\n"), boost::token_compress_on);
+               auto old_content = _content;
+               _content = "";
+               bool done = false;
+               for (auto i: lines) {
+                       if (i.find(s) == string::npos || done) {
+                               _content += i + "\n";
+                       } else {
+                               done = true;
+                       }
+               }
+               BOOST_REQUIRE (_content != old_content);
+       }
+
        void delete_lines (string from, string to)
        {
                vector<string> lines;
@@ -191,7 +222,7 @@ private:
 };
 
 
-#if 0
+LIBDCP_DISABLE_WARNINGS
 static
 void
 dump_notes (vector<dcp::VerificationNote> const & notes)
@@ -200,7 +231,7 @@ dump_notes (vector<dcp::VerificationNote> const & notes)
                std::cout << dcp::note_to_string(i) << "\n";
        }
 }
-#endif
+LIBDCP_ENABLE_WARNINGS
 
 
 static
@@ -208,10 +239,19 @@ void
 check_verify_result (vector<path> dir, vector<dcp::VerificationNote> test_notes)
 {
        auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
-       BOOST_REQUIRE_EQUAL (notes.size(), test_notes.size());
-       for (auto i = 0U; i < notes.size(); ++i) {
-               BOOST_REQUIRE_EQUAL (notes[i], test_notes[i]);
+       std::sort (notes.begin(), notes.end());
+       std::sort (test_notes.begin(), test_notes.end());
+
+       string message = "\nVerification notes from test:\n";
+       for (auto i: notes) {
+               message += "  " + note_to_string(i) + "\n";
+       }
+       message += "Expected:\n";
+       for (auto i: test_notes) {
+               message += "  " + note_to_string(i) + "\n";
        }
+
+       BOOST_REQUIRE_MESSAGE (notes == test_notes, message);
 }
 
 
@@ -339,9 +379,9 @@ BOOST_AUTO_TEST_CASE (verify_mismatched_picture_sound_hashes)
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, dcp_test1_cpl_id, canonical(dir / dcp_test1_cpl) },
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_PICTURE_HASHES, canonical(dir / "video.mxf") },
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_SOUND_HASHES, canonical(dir / "audio.mxf") },
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, "value 'xSEEi70vx1WQs67bmu2zKvzIkXvY=' is invalid Base64-encoded binary", canonical(dir / dcp_test1_pkl), 12 },
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, "value 'xaddO7je2lZSNQp55qjCWo5DLKFQ=' is invalid Base64-encoded binary", canonical(dir / dcp_test1_pkl), 19 },
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, "value 'xWU0/u1wM17y7Kriq06+65/ViX1o=' is invalid Base64-encoded binary", canonical(dir / dcp_test1_pkl), 26 }
+                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, "value 'xLq7ot/GobgrqUYdlbR8FCD5APqs=' is invalid Base64-encoded binary", canonical(dir / dcp_test1_pkl), 26 },
+                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, "value 'xgVKhC9IkWyzQbgzpFcJ1bpqbtwk=' is invalid Base64-encoded binary", canonical(dir / dcp_test1_pkl), 19 },
+                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, "value 'xc1DRq6GaSzV2brF0YnSNed46nqk=' is invalid Base64-encoded binary", canonical(dir / dcp_test1_pkl), 12 }
                });
 }
 
@@ -704,6 +744,7 @@ BOOST_AUTO_TEST_CASE (verify_invalid_smpte_subtitles)
 
        path const dir("build/test/verify_invalid_smpte_subtitles");
        prepare_directory (dir);
+       /* This broken_smpte.mxf does not use urn:uuid: for its subtitle ID, which we tolerate (rightly or wrongly) */
        copy_file ("test/data/broken_smpte.mxf", dir / "subs.mxf");
        auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
        auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
@@ -726,6 +767,63 @@ BOOST_AUTO_TEST_CASE (verify_invalid_smpte_subtitles)
 }
 
 
+BOOST_AUTO_TEST_CASE (verify_empty_text_node_in_subtitles)
+{
+       path const dir("build/test/verify_empty_text_node_in_subtitles");
+       prepare_directory (dir);
+       copy_file ("test/data/empty_text.mxf", dir / "subs.mxf");
+       auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
+       auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 192, 0);
+       auto cpl = write_dcp_with_single_asset (dir, reel_asset);
+
+       check_verify_result (
+               { dir },
+               {
+                       { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EMPTY_TEXT },
+                       { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
+                       { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, canonical(dir / "subs.mxf") },
+                       { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
+               });
+}
+
+
+/** A <Text> node with no content except some <Font> nodes, which themselves do have content */
+BOOST_AUTO_TEST_CASE (verify_empty_text_node_in_subtitles_with_child_nodes)
+{
+       path const dir("build/test/verify_empty_text_node_in_subtitles_with_child_nodes");
+       prepare_directory (dir);
+       copy_file ("test/data/empty_but_with_children.xml", dir / "subs.xml");
+       auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
+       auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 192, 0);
+       auto cpl = write_dcp_with_single_asset (dir, reel_asset, dcp::Standard::INTEROP);
+
+       check_verify_result (
+               { dir },
+               {
+                       { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
+               });
+}
+
+
+/** A <Text> node with no content except some <Font> nodes, which themselves also have no content */
+BOOST_AUTO_TEST_CASE (verify_empty_text_node_in_subtitles_with_empty_child_nodes)
+{
+       path const dir("build/test/verify_empty_text_node_in_subtitles_with_empty_child_nodes");
+       prepare_directory (dir);
+       copy_file ("test/data/empty_with_empty_children.xml", dir / "subs.xml");
+       auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
+       auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 192, 0);
+       auto cpl = write_dcp_with_single_asset (dir, reel_asset, dcp::Standard::INTEROP);
+
+       check_verify_result (
+               { dir },
+               {
+                       { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
+                       { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EMPTY_TEXT },
+               });
+}
+
+
 BOOST_AUTO_TEST_CASE (verify_external_asset)
 {
        path const ov_dir("build/test/verify_external_asset");
@@ -924,7 +1022,7 @@ BOOST_AUTO_TEST_CASE (verify_invalid_language2)
        auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
        asset->_language = "wrong-andbad";
        asset->write (dir / "subs.mxf");
-       auto reel_asset = make_shared<dcp::ReelClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
+       auto reel_asset = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
        reel_asset->_language = "badlang";
        auto cpl = write_dcp_with_single_asset (dir, reel_asset);
 
@@ -1137,7 +1235,7 @@ BOOST_AUTO_TEST_CASE (verify_picture_size)
 
 static
 void
-add_test_subtitle (shared_ptr<dcp::SubtitleAsset> asset, int start_frame, int end_frame, float v_position = 0, string text = "Hello")
+add_test_subtitle (shared_ptr<dcp::SubtitleAsset> asset, int start_frame, int end_frame, float v_position = 0, dcp::VAlign v_align = dcp::VAlign::CENTER, string text = "Hello")
 {
        asset->add (
                make_shared<dcp::SubtitleString>(
@@ -1153,13 +1251,14 @@ add_test_subtitle (shared_ptr<dcp::SubtitleAsset> asset, int start_frame, int en
                        0,
                        dcp::HAlign::CENTER,
                        v_position,
-                       dcp::VAlign::CENTER,
+                       v_align,
                        dcp::Direction::LTR,
                        text,
                        dcp::Effect::NONE,
                        dcp::Colour(),
                        dcp::Time(),
-                       dcp::Time()
+                       dcp::Time(),
+                       0
                )
        );
 }
@@ -1176,7 +1275,7 @@ BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_xml_size_in_bytes)
        }
        asset->set_language (dcp::LanguageTag("de-DE"));
        asset->write (dir / "subs.mxf");
-       auto reel_asset = make_shared<dcp::ReelClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 49148, 0);
+       auto reel_asset = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 49148, 0);
        auto cpl = write_dcp_with_single_asset (dir, reel_asset);
 
        check_verify_result (
@@ -1186,7 +1285,7 @@ BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_xml_size_in_bytes)
                        {
                                dcp::VerificationNote::Type::BV21_ERROR,
                                dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES,
-                               string("413262"),
+                               string("372207"),
                                canonical(dir / "subs.mxf")
                        },
                        { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
@@ -1226,7 +1325,7 @@ verify_timed_text_asset_too_large (string name)
        check_verify_result (
                { dir },
                {
-                       { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, string("121696411"), canonical(dir / "subs.mxf") },
+                       { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, string("121695136"), canonical(dir / "subs.mxf") },
                        { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, string("121634816"), canonical(dir / "subs.mxf") },
                        { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
                        { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
@@ -1238,7 +1337,7 @@ verify_timed_text_asset_too_large (string name)
 BOOST_AUTO_TEST_CASE (verify_subtitle_asset_too_large)
 {
        verify_timed_text_asset_too_large<dcp::ReelSMPTESubtitleAsset>("verify_subtitle_asset_too_large");
-       verify_timed_text_asset_too_large<dcp::ReelClosedCaptionAsset>("verify_closed_caption_asset_too_large");
+       verify_timed_text_asset_too_large<dcp::ReelSMPTEClosedCaptionAsset>("verify_closed_caption_asset_too_large");
 }
 
 
@@ -1277,7 +1376,7 @@ BOOST_AUTO_TEST_CASE (verify_missing_subtitle_language)
        subs->write (dir / "subs.mxf");
 
        auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
-       dcp->cpls().front()->reels().front()->add(reel_subs);
+       dcp->cpls()[0]->reels()[0]->add(reel_subs);
        dcp->write_xml (
                dcp::String::compose("libdcp %1", dcp::version),
                dcp::String::compose("libdcp %1", dcp::version),
@@ -1348,7 +1447,7 @@ BOOST_AUTO_TEST_CASE (verify_multiple_closed_caption_languages_allowed)
                ccaps->set_language (dcp::LanguageTag("de-DE"));
                ccaps->add (simple_subtitle());
                ccaps->write (path / "subs1.mxf");
-               auto reel_ccaps = make_shared<dcp::ReelClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
+               auto reel_ccaps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
                cpl->reels()[0]->add(reel_ccaps);
        }
 
@@ -1357,7 +1456,7 @@ BOOST_AUTO_TEST_CASE (verify_multiple_closed_caption_languages_allowed)
                ccaps->set_language (dcp::LanguageTag("en-US"));
                ccaps->add (simple_subtitle());
                ccaps->write (path / "subs2.mxf");
-               auto reel_ccaps = make_shared<dcp::ReelClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
+               auto reel_ccaps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
                cpl->reels()[1]->add(reel_ccaps);
        }
 
@@ -1412,7 +1511,7 @@ BOOST_AUTO_TEST_CASE (verify_missing_subtitle_start_time)
        subs->write (dir / "subs.mxf");
 
        auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
-       dcp->cpls().front()->reels().front()->add(reel_subs);
+       dcp->cpls()[0]->reels()[0]->add(reel_subs);
        dcp->write_xml (
                dcp::String::compose("libdcp %1", dcp::version),
                dcp::String::compose("libdcp %1", dcp::version),
@@ -1485,16 +1584,18 @@ BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_start_time)
 class TestText
 {
 public:
-       TestText (int in_, int out_, float v_position_ = 0, string text_ = "Hello")
+       TestText (int in_, int out_, float v_position_ = 0, dcp::VAlign v_align_ = dcp::VAlign::CENTER, string text_ = "Hello")
                : in(in_)
                , out(out_)
                , v_position(v_position_)
+               , v_align(v_align_)
                , text(text_)
        {}
 
        int in;
        int out;
        float v_position;
+       dcp::VAlign v_align;
        string text;
 };
 
@@ -1507,7 +1608,7 @@ dcp_with_text (path dir, vector<TestText> subs)
        auto asset = make_shared<dcp::SMPTESubtitleAsset>();
        asset->set_start_time (dcp::Time());
        for (auto i: subs) {
-               add_test_subtitle (asset, i.in, i.out, i.v_position, i.text);
+               add_test_subtitle (asset, i.in, i.out, i.v_position, i.v_align, i.text);
        }
        asset->set_language (dcp::LanguageTag("de-DE"));
        asset->write (dir / "subs.mxf");
@@ -1517,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");
@@ -1671,10 +1809,10 @@ BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count1)
        auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
                dir,
                {
-                       { 96, 200, 0.0, "We" },
-                       { 96, 200, 0.1, "have" },
-                       { 96, 200, 0.2, "four" },
-                       { 96, 200, 0.3, "lines" }
+                       { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
+                       { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
+                       { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
+                       { 96, 200, 0.3, dcp::VAlign::CENTER, "lines" }
                });
        check_verify_result (
                {dir},
@@ -1691,9 +1829,9 @@ BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count1)
        auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
                dir,
                {
-                       { 96, 200, 0.0, "We" },
-                       { 96, 200, 0.1, "have" },
-                       { 96, 200, 0.2, "four" },
+                       { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
+                       { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
+                       { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
                });
        check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
 }
@@ -1705,10 +1843,10 @@ BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count2)
        auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
                dir,
                {
-                       { 96, 300, 0.0, "We" },
-                       { 96, 300, 0.1, "have" },
-                       { 150, 180, 0.2, "four" },
-                       { 150, 180, 0.3, "lines" }
+                       { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
+                       { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
+                       { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
+                       { 150, 180, 0.3, dcp::VAlign::CENTER, "lines" }
                });
        check_verify_result (
                {dir},
@@ -1725,10 +1863,10 @@ BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count2)
        auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
                dir,
                {
-                       { 96, 300, 0.0, "We" },
-                       { 96, 300, 0.1, "have" },
-                       { 150, 180, 0.2, "four" },
-                       { 190, 250, 0.3, "lines" }
+                       { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
+                       { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
+                       { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
+                       { 190, 250, 0.3, dcp::VAlign::CENTER, "lines" }
                });
        check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
 }
@@ -1740,7 +1878,7 @@ BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length1)
        auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
                dir,
                {
-                       { 96, 300, 0.0, "012345678901234567890123456789012345678901234567890123" }
+                       { 96, 300, 0.0, dcp::VAlign::CENTER, "012345678901234567890123456789012345678901234567890123" }
                });
        check_verify_result (
                {dir},
@@ -1757,7 +1895,7 @@ BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length2)
        auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
                dir,
                {
-                       { 96, 300, 0.0, "012345678901234567890123456789012345678901234567890123456789012345678901234567890" }
+                       { 96, 300, 0.0, dcp::VAlign::CENTER, "012345678901234567890123456789012345678901234567890123456789012345678901234567890" }
                });
        check_verify_result (
                {dir},
@@ -1771,13 +1909,13 @@ BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length2)
 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count1)
 {
        auto const dir = path ("build/test/verify_valid_closed_caption_line_count1");
-       auto cpl = dcp_with_text<dcp::ReelClosedCaptionAsset> (
+       auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
                dir,
                {
-                       { 96, 200, 0.0, "We" },
-                       { 96, 200, 0.1, "have" },
-                       { 96, 200, 0.2, "four" },
-                       { 96, 200, 0.3, "lines" }
+                       { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
+                       { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
+                       { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
+                       { 96, 200, 0.3, dcp::VAlign::CENTER, "lines" }
                });
        check_verify_result (
                {dir},
@@ -1791,12 +1929,12 @@ BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count1)
 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count2)
 {
        auto const dir = path ("build/test/verify_valid_closed_caption_line_count2");
-       auto cpl = dcp_with_text<dcp::ReelClosedCaptionAsset> (
+       auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
                dir,
                {
-                       { 96, 200, 0.0, "We" },
-                       { 96, 200, 0.1, "have" },
-                       { 96, 200, 0.2, "four" },
+                       { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
+                       { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
+                       { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
                });
        check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
 }
@@ -1805,13 +1943,13 @@ BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count2)
 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_count3)
 {
        auto const dir = path ("build/test/verify_invalid_closed_caption_line_count3");
-       auto cpl = dcp_with_text<dcp::ReelClosedCaptionAsset> (
+       auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
                dir,
                {
-                       { 96, 300, 0.0, "We" },
-                       { 96, 300, 0.1, "have" },
-                       { 150, 180, 0.2, "four" },
-                       { 150, 180, 0.3, "lines" }
+                       { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
+                       { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
+                       { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
+                       { 150, 180, 0.3, dcp::VAlign::CENTER, "lines" }
                });
        check_verify_result (
                {dir},
@@ -1825,13 +1963,13 @@ BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_count3)
 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count4)
 {
        auto const dir = path ("build/test/verify_valid_closed_caption_line_count4");
-       auto cpl = dcp_with_text<dcp::ReelClosedCaptionAsset> (
+       auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
                dir,
                {
-                       { 96, 300, 0.0, "We" },
-                       { 96, 300, 0.1, "have" },
-                       { 150, 180, 0.2, "four" },
-                       { 190, 250, 0.3, "lines" }
+                       { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
+                       { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
+                       { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
+                       { 190, 250, 0.3, dcp::VAlign::CENTER, "lines" }
                });
        check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
 }
@@ -1840,10 +1978,10 @@ BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count4)
 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_length)
 {
        auto const dir = path ("build/test/verify_invalid_closed_caption_line_length");
-       auto cpl = dcp_with_text<dcp::ReelClosedCaptionAsset> (
+       auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
                dir,
                {
-                       { 96, 300, 0.0, "0123456789012345678901234567890123" }
+                       { 96, 300, 0.0, dcp::VAlign::CENTER, "0123456789012345678901234567890123" }
                });
        check_verify_result (
                {dir},
@@ -1854,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");
@@ -1863,7 +2100,7 @@ BOOST_AUTO_TEST_CASE (verify_invalid_sound_frame_rate)
        auto reel_picture = make_shared<dcp::ReelMonoPictureAsset>(picture, 0);
        auto reel = make_shared<dcp::Reel>();
        reel->add (reel_picture);
-       auto sound = simple_sound (dir, "foo", dcp::MXFMetadata(), "de-DE", 24, 96000);
+       auto sound = simple_sound (dir, "foo", dcp::MXFMetadata(), "de-DE", 24, 96000, boost::none);
        auto reel_sound = make_shared<dcp::ReelSoundAsset>(sound, 0);
        reel->add (reel_sound);
        reel->add (simple_markers());
@@ -2091,7 +2328,7 @@ verify_closed_captions_must_be_in_all_reels_check (path dir, int caps_in_reel1,
                );
 
        for (int i = 0; i < caps_in_reel1; ++i) {
-               reel1->add (make_shared<dcp::ReelClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
+               reel1->add (make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
        }
 
        auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length, 0);
@@ -2106,7 +2343,7 @@ verify_closed_captions_must_be_in_all_reels_check (path dir, int caps_in_reel1,
                );
 
        for (int i = 0; i < caps_in_reel2; ++i) {
-               reel2->add (make_shared<dcp::ReelClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
+               reel2->add (make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
        }
 
        auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length, 0);
@@ -2218,18 +2455,18 @@ BOOST_AUTO_TEST_CASE (verify_text_entry_point)
                        }
                );
 
-       verify_text_entry_point_check<dcp::ReelClosedCaptionAsset> (
+       verify_text_entry_point_check<dcp::ReelSMPTEClosedCaptionAsset> (
                "build/test/verify_closed_caption_entry_point_must_be_present",
                dcp::VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT,
-               [](shared_ptr<dcp::ReelClosedCaptionAsset> asset) {
+               [](shared_ptr<dcp::ReelSMPTEClosedCaptionAsset> asset) {
                        asset->unset_entry_point ();
                        }
                );
 
-       verify_text_entry_point_check<dcp::ReelClosedCaptionAsset> (
+       verify_text_entry_point_check<dcp::ReelSMPTEClosedCaptionAsset> (
                "build/test/verify_closed_caption_entry_point_must_be_zero",
                dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT,
-               [](shared_ptr<dcp::ReelClosedCaptionAsset> asset) {
+               [](shared_ptr<dcp::ReelSMPTEClosedCaptionAsset> asset) {
                        asset->set_entry_point (9);
                        }
                );
@@ -2251,18 +2488,21 @@ BOOST_AUTO_TEST_CASE (verify_missing_hash)
 
        BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
        auto const cpl = dcp->cpls()[0];
+       BOOST_REQUIRE_EQUAL (cpl->reels().size(), 1U);
+       BOOST_REQUIRE (cpl->reels()[0]->main_picture());
+       auto asset_id = cpl->reels()[0]->main_picture()->id();
 
        {
                BOOST_REQUIRE (cpl->file());
                Editor e(cpl->file().get());
-               e.replace("<Hash>addO7je2lZSNQp55qjCWo5DLKFQ=</Hash>", "");
+               e.delete_first_line_containing("<Hash>");
        }
 
        check_verify_result (
                {dir},
                {
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
-                       { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_HASH, string("1fab8bb0-cfaf-4225-ad6d-01768bc10470") }
+                       { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_HASH, asset_id }
                });
 }
 
@@ -2405,6 +2645,7 @@ BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata1)
                "A Test DCP"
                );
 
+       BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
        auto cpl = dcp->cpls()[0];
 
        {
@@ -2949,3 +3190,23 @@ BOOST_AUTO_TEST_CASE (verify_incorrect_timed_text_id)
                        { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
                });
 }
+
+
+/** Check a DCP with a 3D asset marked as 2D */
+BOOST_AUTO_TEST_CASE (verify_threed_marked_as_twod)
+{
+       check_verify_result (
+               { private_test / "data" / "xm" },
+               {
+                       {
+                               dcp::VerificationNote::Type::WARNING,
+                               dcp::VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD, boost::filesystem::canonical(find_file(private_test / "data" / "xm", "j2c"))
+                       },
+                       {
+                               dcp::VerificationNote::Type::BV21_ERROR,
+                               dcp::VerificationNote::Code::INVALID_STANDARD
+                       },
+               });
+
+}
+