Fix various bugs in subtitle/ccap verification.
[libdcp.git] / src / verify.cc
index fe071b48ce39353a566acc1836e28e848f19e6ff..97758e6192e632aae3bf4c498c167a90120818ad 100644 (file)
     files in the program, then also delete it here.
 */
 
+
+/** @file  src/verify.cc
+ *  @brief dcp::verify() method and associated code
+ */
+
+
 #include "verify.h"
 #include "dcp.h"
 #include "cpl.h"
 #include <xercesc/dom/DOMErrorHandler.hpp>
 #include <xercesc/framework/LocalFileInputSource.hpp>
 #include <xercesc/framework/MemBufInputSource.hpp>
-#include <boost/noncopyable.hpp>
 #include <boost/algorithm/string.hpp>
 #include <map>
 #include <vector>
 #include <iostream>
 
+
 using std::list;
 using std::vector;
 using std::string;
@@ -86,9 +92,11 @@ using boost::optional;
 using boost::function;
 using std::dynamic_pointer_cast;
 
+
 using namespace dcp;
 using namespace xercesc;
 
+
 static
 string
 xml_ch_to_string (XMLCh const * a)
@@ -99,6 +107,7 @@ xml_ch_to_string (XMLCh const * a)
        return o;
 }
 
+
 class XMLValidationError
 {
 public:
@@ -184,7 +193,8 @@ private:
        list<XMLValidationError> _errors;
 };
 
-class StringToXMLCh : public boost::noncopyable
+
+class StringToXMLCh
 {
 public:
        StringToXMLCh (string a)
@@ -192,6 +202,9 @@ public:
                _buffer = XMLString::transcode(a.c_str());
        }
 
+       StringToXMLCh (StringToXMLCh const&) = delete;
+       StringToXMLCh& operator= (StringToXMLCh const&) = delete;
+
        ~StringToXMLCh ()
        {
                XMLString::release (&_buffer);
@@ -205,6 +218,7 @@ private:
        XMLCh* _buffer;
 };
 
+
 class LocalFileResolver : public EntityResolver
 {
 public:
@@ -362,19 +376,19 @@ enum class VerifyAssetResult {
 
 
 static VerifyAssetResult
-verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelMXF> reel_mxf, function<void (float)> progress)
+verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelFileAsset> reel_file_asset, function<void (float)> progress)
 {
-       auto const actual_hash = reel_mxf->asset_ref()->hash(progress);
+       auto const actual_hash = reel_file_asset->asset_ref()->hash(progress);
 
        auto pkls = dcp->pkls();
        /* We've read this DCP in so it must have at least one PKL */
        DCP_ASSERT (!pkls.empty());
 
-       auto asset = reel_mxf->asset_ref().asset();
+       auto asset = reel_file_asset->asset_ref().asset();
 
        optional<string> pkl_hash;
        for (auto i: pkls) {
-               pkl_hash = i->hash (reel_mxf->asset_ref()->id());
+               pkl_hash = i->hash (reel_file_asset->asset_ref()->id());
                if (pkl_hash) {
                        break;
                }
@@ -382,7 +396,7 @@ verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelMXF> reel_mxf, fun
 
        DCP_ASSERT (pkl_hash);
 
-       auto cpl_hash = reel_mxf->hash();
+       auto cpl_hash = reel_file_asset->hash();
        if (cpl_hash && *cpl_hash != *pkl_hash) {
                return VerifyAssetResult::CPL_PKL_DIFFER;
        }
@@ -429,9 +443,9 @@ biggest_frame_size (shared_ptr<const StereoPictureFrame> frame)
 
 template <class A, class R, class F>
 optional<VerifyPictureAssetResult>
-verify_picture_asset_type (shared_ptr<const ReelMXF> reel_mxf, function<void (float)> progress)
+verify_picture_asset_type (shared_ptr<const ReelFileAsset> reel_file_asset, function<void (float)> progress)
 {
-       auto asset = dynamic_pointer_cast<A>(reel_mxf->asset_ref().asset());
+       auto asset = dynamic_pointer_cast<A>(reel_file_asset->asset_ref().asset());
        if (!asset) {
                return optional<VerifyPictureAssetResult>();
        }
@@ -458,11 +472,11 @@ verify_picture_asset_type (shared_ptr<const ReelMXF> reel_mxf, function<void (fl
 
 
 static VerifyPictureAssetResult
-verify_picture_asset (shared_ptr<const ReelMXF> reel_mxf, function<void (float)> progress)
+verify_picture_asset (shared_ptr<const ReelFileAsset> reel_file_asset, function<void (float)> progress)
 {
-       auto r = verify_picture_asset_type<MonoPictureAsset, MonoPictureAssetReader, MonoPictureFrame>(reel_mxf, progress);
+       auto r = verify_picture_asset_type<MonoPictureAsset, MonoPictureAssetReader, MonoPictureFrame>(reel_file_asset, progress);
        if (!r) {
-               r = verify_picture_asset_type<StereoPictureAsset, StereoPictureAssetReader, StereoPictureFrame>(reel_mxf, progress);
+               r = verify_picture_asset_type<StereoPictureAsset, StereoPictureAssetReader, StereoPictureFrame>(reel_file_asset, progress);
        }
 
        DCP_ASSERT (r);
@@ -637,31 +651,26 @@ struct State
 };
 
 
-
+/** Verify stuff that is common to both subtitles and closed captions */
 void
-verify_smpte_subtitle_asset (
+verify_smpte_timed_text_asset (
        shared_ptr<const SMPTESubtitleAsset> asset,
-       vector<VerificationNote>& notes,
-       State& state
+       vector<VerificationNote>& notes
        )
 {
        if (asset->language()) {
-               auto const language = *asset->language();
-               verify_language_tag (language, notes);
-               if (!state.subtitle_language) {
-                       state.subtitle_language = language;
-               } else if (state.subtitle_language != language) {
-                       notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
-               }
+               verify_language_tag (*asset->language(), notes);
        } else {
                notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
        }
+
        auto const size = boost::filesystem::file_size(asset->file().get());
        if (size > 115 * 1024 * 1024) {
                notes.push_back (
                        { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file() }
                        );
        }
+
        /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
         * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
         */
@@ -682,6 +691,25 @@ verify_smpte_subtitle_asset (
 }
 
 
+/** Verify SMPTE subtitle-only stuff */
+void
+verify_smpte_subtitle_asset (
+       shared_ptr<const SMPTESubtitleAsset> asset,
+       vector<VerificationNote>& notes,
+       State& state
+       )
+{
+       if (asset->language()) {
+               if (!state.subtitle_language) {
+                       state.subtitle_language = *asset->language();
+               } else if (state.subtitle_language != *asset->language()) {
+                       notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
+               }
+       }
+}
+
+
+/** Verify all subtitle stuff */
 static void
 verify_subtitle_asset (
        shared_ptr<const SubtitleAsset> asset,
@@ -699,21 +727,31 @@ verify_subtitle_asset (
 
        auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
        if (smpte) {
+               verify_smpte_timed_text_asset (smpte, notes);
                verify_smpte_subtitle_asset (smpte, notes, state);
        }
 }
 
 
+/** Verify all closed caption stuff */
 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
+       vector<VerificationNote>& notes
        )
 {
-       verify_subtitle_asset (asset, stage, xsd_dtd_directory, notes, state);
+       stage ("Checking closed caption XML", asset->file());
+       /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
+        * gets passed through libdcp which may clean up and therefore hide errors.
+        */
+       validate_xml (asset->raw_xml(), xsd_dtd_directory, notes);
+
+       auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
+       if (smpte) {
+               verify_smpte_timed_text_asset (smpte, notes);
+       }
 
        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()});
@@ -725,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,
@@ -737,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);
                        }
                }
        };
@@ -776,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) {
@@ -800,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
+               });
+       }
 }
 
 
@@ -910,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());
                        },
@@ -930,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();
                        },
@@ -1008,11 +1074,11 @@ pkl_has_encrypted_assets (shared_ptr<DCP> dcp, shared_ptr<PKL> pkl)
 {
        vector<string> encrypted;
        for (auto i: dcp->cpls()) {
-               for (auto j: i->reel_mxfs()) {
+               for (auto j: i->reel_file_assets()) {
                        if (j->asset_ref().resolved()) {
                                /* It's a bit surprising / broken but Interop subtitle assets are represented
-                                * in reels by ReelSubtitleAsset which inherits ReelMXF, so it's possible for
-                                * ReelMXFs to have assets which are not MXFs.
+                                * in reels by ReelSubtitleAsset which inherits ReelFileAsset, so it's possible for
+                                * ReelFileAssets to have assets which are not MXFs.
                                 */
                                if (auto asset = dynamic_pointer_cast<MXF>(j->asset_ref().asset())) {
                                        if (asset->encrypted()) {
@@ -1152,8 +1218,8 @@ dcp::verify (
                                        if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
                                                notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
                                        }
-                                       auto mxf = dynamic_pointer_cast<ReelMXF>(i);
-                                       if (mxf && !mxf->hash()) {
+                                       auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
+                                       if (file_asset && !file_asset->hash()) {
                                                notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
                                        }
                                }
@@ -1210,7 +1276,7 @@ dcp::verify (
                                for (auto i: reel->closed_captions()) {
                                        verify_closed_caption_reel (i, notes);
                                        if (i->asset_ref().resolved()) {
-                                               verify_closed_caption_asset (i->asset(), stage, xsd_dtd_directory, notes, state);
+                                               verify_closed_caption_asset (i->asset(), stage, xsd_dtd_directory, notes);
                                        }
                                }
 
@@ -1224,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) {
@@ -1254,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()) {
@@ -1339,6 +1405,7 @@ dcp::verify (
        return notes;
 }
 
+
 string
 dcp::note_to_string (VerificationNote note)
 {
@@ -1378,9 +1445,9 @@ dcp::note_to_string (VerificationNote note)
        case VerificationNote::Code::MISSING_ASSETMAP:
                return "No ASSETMAP or ASSETMAP.xml was found.";
        case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
-               return String::compose("The intrinsic duration of the asset %1 is less than 1 second long.", note.note().get());
+               return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
        case VerificationNote::Code::INVALID_DURATION:
-               return String::compose("The duration of the asset %1 is less than 1 second long.", note.note().get());
+               return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
        case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
                return String::compose("The instantaneous bit rate of the picture asset %1 is larger than the limit of 250Mbit/s in at least one place.", note.file()->filename());
        case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
@@ -1419,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:
@@ -1476,9 +1545,35 @@ dcp::note_to_string (VerificationNote note)
        case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
                return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
        case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
-               return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>", note.note().get());
+               return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
        case VerificationNote::Code::PARTIALLY_ENCRYPTED:
-               return "Some assets are encrypted but some are not";
+               return "Some assets are encrypted but some are not.";
+       case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
+               return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1)", note.note().get());
+       case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
+               return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
+       case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
+               return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
+       case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
+               return "The JPEG2000 tile size is not the same as the image size.";
+       case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
+               return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
+       case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
+               return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
+       case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
+               return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
+       case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
+               return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
+       case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
+               return String::compose("Incorrect POC marker content found (%1)", note.note().get());
+       case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
+               return "POC marker found outside main header";
+       case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
+               return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
+       case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
+               return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
+       case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
+               return "No TLM marker was found in a JPEG2000 codestream.";
        }
 
        return "";
@@ -1491,6 +1586,7 @@ dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
        return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
 }
 
+
 std::ostream&
 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
 {