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 {
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);
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();
}
};
}
+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;
}
);
}
+
+ verify_closed_caption_details (reels, notes);
}
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 "";
/** 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)
--- /dev/null
+<?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>
+
--- /dev/null
+<?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>
+
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>",
+ {}
+ );
+}
+
}
+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");
}
+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");