libdcp
verify.cc
Go to the documentation of this file.
1 /*
2  Copyright (C) 2018-2021 Carl Hetherington <cth@carlh.net>
3 
4  This file is part of libdcp.
5 
6  libdcp is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10 
11  libdcp is distributed in the hope that it will be useful,
12  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  GNU General Public License for more details.
15 
16  You should have received a copy of the GNU General Public License
17  along with libdcp. If not, see <http://www.gnu.org/licenses/>.
18 
19  In addition, as a special exception, the copyright holders give
20  permission to link the code of portions of this program with the
21  OpenSSL library under certain conditions as described in each
22  individual source file, and distribute linked combinations
23  including the two.
24 
25  You must obey the GNU General Public License in all respects
26  for all of the code used other than OpenSSL. If you modify
27  file(s) with this exception, you may extend this exception to your
28  version of the file(s), but you are not obligated to do so. If you
29  do not wish to do so, delete this exception statement from your
30  version. If you delete this exception statement from all source
31  files in the program, then also delete it here.
32 */
33 
34 
40 #include "compose.hpp"
41 #include "cpl.h"
42 #include "dcp.h"
43 #include "exceptions.h"
44 #include "interop_subtitle_asset.h"
45 #include "mono_picture_asset.h"
46 #include "mono_picture_frame.h"
47 #include "raw_convert.h"
48 #include "reel.h"
51 #include "reel_markers_asset.h"
52 #include "reel_picture_asset.h"
53 #include "reel_sound_asset.h"
54 #include "reel_smpte_subtitle_asset.h"
55 #include "reel_subtitle_asset.h"
56 #include "smpte_subtitle_asset.h"
57 #include "stereo_picture_asset.h"
58 #include "stereo_picture_frame.h"
59 #include "verify.h"
60 #include "verify_j2k.h"
61 #include <xercesc/dom/DOMAttr.hpp>
62 #include <xercesc/dom/DOMDocument.hpp>
63 #include <xercesc/dom/DOMError.hpp>
64 #include <xercesc/dom/DOMErrorHandler.hpp>
65 #include <xercesc/dom/DOMException.hpp>
66 #include <xercesc/dom/DOMImplementation.hpp>
67 #include <xercesc/dom/DOMImplementationLS.hpp>
68 #include <xercesc/dom/DOMImplementationRegistry.hpp>
69 #include <xercesc/dom/DOMLSParser.hpp>
70 #include <xercesc/dom/DOMLocator.hpp>
71 #include <xercesc/dom/DOMNamedNodeMap.hpp>
72 #include <xercesc/dom/DOMNodeList.hpp>
73 #include <xercesc/framework/LocalFileInputSource.hpp>
74 #include <xercesc/framework/MemBufInputSource.hpp>
75 #include <xercesc/parsers/AbstractDOMParser.hpp>
76 #include <xercesc/parsers/XercesDOMParser.hpp>
77 #include <xercesc/sax/HandlerBase.hpp>
78 #include <xercesc/util/PlatformUtils.hpp>
79 #include <boost/algorithm/string.hpp>
80 #include <iostream>
81 #include <map>
82 #include <vector>
83 
84 
85 using std::list;
86 using std::vector;
87 using std::string;
88 using std::cout;
89 using std::map;
90 using std::max;
91 using std::shared_ptr;
92 using std::make_shared;
93 using boost::optional;
94 using boost::function;
95 using std::dynamic_pointer_cast;
96 
97 
98 using namespace dcp;
99 using namespace xercesc;
100 
101 
102 static
103 string
104 xml_ch_to_string (XMLCh const * a)
105 {
106  char* x = XMLString::transcode(a);
107  string const o(x);
108  XMLString::release(&x);
109  return o;
110 }
111 
112 
114 {
115 public:
116  XMLValidationError (SAXParseException const & e)
117  : _message (xml_ch_to_string(e.getMessage()))
118  , _line (e.getLineNumber())
119  , _column (e.getColumnNumber())
120  , _public_id (e.getPublicId() ? xml_ch_to_string(e.getPublicId()) : "")
121  , _system_id (e.getSystemId() ? xml_ch_to_string(e.getSystemId()) : "")
122  {
123 
124  }
125 
126  string message () const {
127  return _message;
128  }
129 
130  uint64_t line () const {
131  return _line;
132  }
133 
134  uint64_t column () const {
135  return _column;
136  }
137 
138  string public_id () const {
139  return _public_id;
140  }
141 
142  string system_id () const {
143  return _system_id;
144  }
145 
146 private:
147  string _message;
148  uint64_t _line;
149  uint64_t _column;
150  string _public_id;
151  string _system_id;
152 };
153 
154 
155 class DCPErrorHandler : public ErrorHandler
156 {
157 public:
158  void warning(const SAXParseException& e) override
159  {
160  maybe_add (XMLValidationError(e));
161  }
162 
163  void error(const SAXParseException& e) override
164  {
165  maybe_add (XMLValidationError(e));
166  }
167 
168  void fatalError(const SAXParseException& e) override
169  {
170  maybe_add (XMLValidationError(e));
171  }
172 
173  void resetErrors() override {
174  _errors.clear ();
175  }
176 
177  list<XMLValidationError> errors () const {
178  return _errors;
179  }
180 
181 private:
182  void maybe_add (XMLValidationError e)
183  {
184  /* XXX: nasty hack */
185  if (
186  e.message().find("schema document") != string::npos &&
187  e.message().find("has different target namespace from the one specified in instance document") != string::npos
188  ) {
189  return;
190  }
191 
192  _errors.push_back (e);
193  }
194 
195  list<XMLValidationError> _errors;
196 };
197 
198 
200 {
201 public:
202  StringToXMLCh (string a)
203  {
204  _buffer = XMLString::transcode(a.c_str());
205  }
206 
207  StringToXMLCh (StringToXMLCh const&) = delete;
208  StringToXMLCh& operator= (StringToXMLCh const&) = delete;
209 
210  ~StringToXMLCh ()
211  {
212  XMLString::release (&_buffer);
213  }
214 
215  XMLCh const * get () const {
216  return _buffer;
217  }
218 
219 private:
220  XMLCh* _buffer;
221 };
222 
223 
224 class LocalFileResolver : public EntityResolver
225 {
226 public:
227  LocalFileResolver (boost::filesystem::path xsd_dtd_directory)
228  : _xsd_dtd_directory (xsd_dtd_directory)
229  {
230  /* XXX: I'm not clear on what things need to be in this list; some XSDs are apparently, magically
231  * found without being here.
232  */
233  add("http://www.w3.org/2001/XMLSchema.dtd", "XMLSchema.dtd");
234  add("http://www.w3.org/2001/03/xml.xsd", "xml.xsd");
235  add("http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd", "xmldsig-core-schema.xsd");
236  add("http://www.digicine.com/schemas/437-Y/2007/Main-Stereo-Picture-CPL.xsd", "Main-Stereo-Picture-CPL.xsd");
237  add("http://www.digicine.com/PROTO-ASDCP-CPL-20040511.xsd", "PROTO-ASDCP-CPL-20040511.xsd");
238  add("http://www.digicine.com/PROTO-ASDCP-PKL-20040311.xsd", "PROTO-ASDCP-PKL-20040311.xsd");
239  add("http://www.digicine.com/PROTO-ASDCP-AM-20040311.xsd", "PROTO-ASDCP-AM-20040311.xsd");
240  add("http://www.digicine.com/PROTO-ASDCP-CC-CPL-20070926#", "PROTO-ASDCP-CC-CPL-20070926.xsd");
241  add("interop-subs", "DCSubtitle.v1.mattsson.xsd");
242  add("http://www.smpte-ra.org/schemas/428-7/2010/DCST.xsd", "SMPTE-428-7-2010-DCST.xsd");
243  add("http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata", "SMPTE-429-16.xsd");
244  add("http://www.dolby.com/schemas/2012/AD", "Dolby-2012-AD.xsd");
245  add("http://www.smpte-ra.org/schemas/429-10/2008/Main-Stereo-Picture-CPL", "SMPTE-429-10-2008.xsd");
246  }
247 
248  InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id) override
249  {
250  if (!system_id) {
251  return 0;
252  }
253  auto system_id_str = xml_ch_to_string (system_id);
254  auto p = _xsd_dtd_directory;
255  if (_files.find(system_id_str) == _files.end()) {
256  p /= system_id_str;
257  } else {
258  p /= _files[system_id_str];
259  }
260  StringToXMLCh ch (p.string());
261  return new LocalFileInputSource(ch.get());
262  }
263 
264 private:
265  void add (string uri, string file)
266  {
267  _files[uri] = file;
268  }
269 
270  std::map<string, string> _files;
271  boost::filesystem::path _xsd_dtd_directory;
272 };
273 
274 
275 static void
276 parse (XercesDOMParser& parser, boost::filesystem::path xml)
277 {
278  parser.parse(xml.string().c_str());
279 }
280 
281 
282 static void
283 parse (XercesDOMParser& parser, string xml)
284 {
285  xercesc::MemBufInputSource buf(reinterpret_cast<unsigned char const*>(xml.c_str()), xml.size(), "");
286  parser.parse(buf);
287 }
288 
289 
290 template <class T>
291 void
292 validate_xml (T xml, boost::filesystem::path xsd_dtd_directory, vector<VerificationNote>& notes)
293 {
294  try {
295  XMLPlatformUtils::Initialize ();
296  } catch (XMLException& e) {
297  throw MiscError ("Failed to initialise xerces library");
298  }
299 
300  DCPErrorHandler error_handler;
301 
302  /* All the xerces objects in this scope must be destroyed before XMLPlatformUtils::Terminate() is called */
303  {
304  XercesDOMParser parser;
305  parser.setValidationScheme(XercesDOMParser::Val_Always);
306  parser.setDoNamespaces(true);
307  parser.setDoSchema(true);
308 
309  vector<string> schema;
310  schema.push_back("xml.xsd");
311  schema.push_back("xmldsig-core-schema.xsd");
312  schema.push_back("SMPTE-429-7-2006-CPL.xsd");
313  schema.push_back("SMPTE-429-8-2006-PKL.xsd");
314  schema.push_back("SMPTE-429-9-2007-AM.xsd");
315  schema.push_back("Main-Stereo-Picture-CPL.xsd");
316  schema.push_back("PROTO-ASDCP-CPL-20040511.xsd");
317  schema.push_back("PROTO-ASDCP-PKL-20040311.xsd");
318  schema.push_back("PROTO-ASDCP-AM-20040311.xsd");
319  schema.push_back("DCSubtitle.v1.mattsson.xsd");
320  schema.push_back("DCDMSubtitle-2010.xsd");
321  schema.push_back("PROTO-ASDCP-CC-CPL-20070926.xsd");
322  schema.push_back("SMPTE-429-16.xsd");
323  schema.push_back("Dolby-2012-AD.xsd");
324  schema.push_back("SMPTE-429-10-2008.xsd");
325  schema.push_back("xlink.xsd");
326  schema.push_back("SMPTE-335-2012.xsd");
327  schema.push_back("SMPTE-395-2014-13-1-aaf.xsd");
328  schema.push_back("isdcf-mca.xsd");
329  schema.push_back("SMPTE-429-12-2008.xsd");
330 
331  /* XXX: I'm not especially clear what this is for, but it seems to be necessary.
332  * Schemas that are not mentioned in this list are not read, and the things
333  * they describe are not checked.
334  */
335  string locations;
336  for (auto i: schema) {
337  locations += String::compose("%1 %1 ", i, i);
338  }
339 
340  parser.setExternalSchemaLocation(locations.c_str());
341  parser.setValidationSchemaFullChecking(true);
342  parser.setErrorHandler(&error_handler);
343 
344  LocalFileResolver resolver (xsd_dtd_directory);
345  parser.setEntityResolver(&resolver);
346 
347  try {
348  parser.resetDocumentPool();
349  parse(parser, xml);
350  } catch (XMLException& e) {
351  throw MiscError(xml_ch_to_string(e.getMessage()));
352  } catch (DOMException& e) {
353  throw MiscError(xml_ch_to_string(e.getMessage()));
354  } catch (...) {
355  throw MiscError("Unknown exception from xerces");
356  }
357  }
358 
359  XMLPlatformUtils::Terminate ();
360 
361  for (auto i: error_handler.errors()) {
362  notes.push_back ({
363  VerificationNote::Type::ERROR,
365  i.message(),
366  boost::trim_copy(i.public_id() + " " + i.system_id()),
367  i.line()
368  });
369  }
370 }
371 
372 
373 enum class VerifyAssetResult {
374  GOOD,
375  CPL_PKL_DIFFER,
376  BAD
377 };
378 
379 
380 static VerifyAssetResult
381 verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelFileAsset> reel_file_asset, function<void (float)> progress)
382 {
383  auto const actual_hash = reel_file_asset->asset_ref()->hash(progress);
384 
385  auto pkls = dcp->pkls();
386  /* We've read this DCP in so it must have at least one PKL */
387  DCP_ASSERT (!pkls.empty());
388 
389  auto asset = reel_file_asset->asset_ref().asset();
390 
391  optional<string> pkl_hash;
392  for (auto i: pkls) {
393  pkl_hash = i->hash (reel_file_asset->asset_ref()->id());
394  if (pkl_hash) {
395  break;
396  }
397  }
398 
399  DCP_ASSERT (pkl_hash);
400 
401  auto cpl_hash = reel_file_asset->hash();
402  if (cpl_hash && *cpl_hash != *pkl_hash) {
403  return VerifyAssetResult::CPL_PKL_DIFFER;
404  }
405 
406  if (actual_hash != *pkl_hash) {
407  return VerifyAssetResult::BAD;
408  }
409 
410  return VerifyAssetResult::GOOD;
411 }
412 
413 
414 void
415 verify_language_tag (string tag, vector<VerificationNote>& notes)
416 {
417  try {
418  LanguageTag test (tag);
419  } catch (LanguageTagError &) {
421  }
422 }
423 
424 
425 static void
426 verify_picture_asset (shared_ptr<const ReelFileAsset> reel_file_asset, boost::filesystem::path file, vector<VerificationNote>& notes, function<void (float)> progress)
427 {
428  int biggest_frame = 0;
429  auto asset = dynamic_pointer_cast<PictureAsset>(reel_file_asset->asset_ref().asset());
430  auto const duration = asset->intrinsic_duration ();
431 
432  auto check_and_add = [&notes](vector<VerificationNote> const& j2k_notes) {
433  for (auto i: j2k_notes) {
434  if (find(notes.begin(), notes.end(), i) == notes.end()) {
435  notes.push_back (i);
436  }
437  }
438  };
439 
440  if (auto mono_asset = dynamic_pointer_cast<MonoPictureAsset>(reel_file_asset->asset_ref().asset())) {
441  auto reader = mono_asset->start_read ();
442  for (int64_t i = 0; i < duration; ++i) {
443  auto frame = reader->get_frame (i);
444  biggest_frame = max(biggest_frame, frame->size());
445  if (!mono_asset->encrypted() || mono_asset->key()) {
446  vector<VerificationNote> j2k_notes;
447  verify_j2k (frame, j2k_notes);
448  check_and_add (j2k_notes);
449  }
450  progress (float(i) / duration);
451  }
452  } else if (auto stereo_asset = dynamic_pointer_cast<StereoPictureAsset>(asset)) {
453  auto reader = stereo_asset->start_read ();
454  for (int64_t i = 0; i < duration; ++i) {
455  auto frame = reader->get_frame (i);
456  biggest_frame = max(biggest_frame, max(frame->left()->size(), frame->right()->size()));
457  if (!stereo_asset->encrypted() || mono_asset->key()) {
458  vector<VerificationNote> j2k_notes;
459  verify_j2k (frame->left(), j2k_notes);
460  verify_j2k (frame->right(), j2k_notes);
461  check_and_add (j2k_notes);
462  }
463  progress (float(i) / duration);
464  }
465 
466  }
467 
468  static const int max_frame = rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
469  static const int risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
470  if (biggest_frame > max_frame) {
471  notes.push_back ({
472  VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
473  });
474  } else if (biggest_frame > risky_frame) {
475  notes.push_back ({
476  VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
477  });
478  }
479 }
480 
481 
482 static void
483 verify_main_picture_asset (
484  shared_ptr<const DCP> dcp,
485  shared_ptr<const ReelPictureAsset> reel_asset,
486  function<void (string, optional<boost::filesystem::path>)> stage,
487  function<void (float)> progress,
488  vector<VerificationNote>& notes
489  )
490 {
491  auto asset = reel_asset->asset();
492  auto const file = *asset->file();
493  stage ("Checking picture asset hash", file);
494  auto const r = verify_asset (dcp, reel_asset, progress);
495  switch (r) {
496  case VerifyAssetResult::BAD:
497  notes.push_back ({
498  VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file
499  });
500  break;
501  case VerifyAssetResult::CPL_PKL_DIFFER:
502  notes.push_back ({
503  VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file
504  });
505  break;
506  default:
507  break;
508  }
509  stage ("Checking picture frame sizes", asset->file());
510  verify_picture_asset (reel_asset, file, notes, progress);
511 
512  /* Only flat/scope allowed by Bv2.1 */
513  if (
514  asset->size() != Size(2048, 858) &&
515  asset->size() != Size(1998, 1080) &&
516  asset->size() != Size(4096, 1716) &&
517  asset->size() != Size(3996, 2160)) {
518  notes.push_back({
521  String::compose("%1x%2", asset->size().width, asset->size().height),
522  file
523  });
524  }
525 
526  /* Only 24, 25, 48fps allowed for 2K */
527  if (
528  (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
529  (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
530  ) {
531  notes.push_back({
534  String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
535  file
536  });
537  }
538 
539  if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
540  /* Only 24fps allowed for 4K */
541  if (asset->edit_rate() != Fraction(24, 1)) {
542  notes.push_back({
545  String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
546  file
547  });
548  }
549 
550  /* Only 2D allowed for 4K */
551  if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
552  notes.push_back({
555  String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
556  file
557  });
558 
559  }
560  }
561 
562 }
563 
564 
565 static void
566 verify_main_sound_asset (
567  shared_ptr<const DCP> dcp,
568  shared_ptr<const ReelSoundAsset> reel_asset,
569  function<void (string, optional<boost::filesystem::path>)> stage,
570  function<void (float)> progress,
571  vector<VerificationNote>& notes
572  )
573 {
574  auto asset = reel_asset->asset();
575  stage ("Checking sound asset hash", asset->file());
576  auto const r = verify_asset (dcp, reel_asset, progress);
577  switch (r) {
578  case VerifyAssetResult::BAD:
579  notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, *asset->file()});
580  break;
581  case VerifyAssetResult::CPL_PKL_DIFFER:
582  notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, *asset->file()});
583  break;
584  default:
585  break;
586  }
587 
588  stage ("Checking sound asset metadata", asset->file());
589 
590  if (auto lang = asset->language()) {
591  verify_language_tag (*lang, notes);
592  }
593  if (asset->sampling_rate() != 48000) {
594  notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), *asset->file()});
595  }
596 }
597 
598 
599 static void
600 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
601 {
602  /* XXX: is Language compulsory? */
603  if (reel_asset->language()) {
604  verify_language_tag (*reel_asset->language(), notes);
605  }
606 
607  if (!reel_asset->entry_point()) {
609  } else if (reel_asset->entry_point().get()) {
611  }
612 }
613 
614 
615 static void
616 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
617 {
618  /* XXX: is Language compulsory? */
619  if (reel_asset->language()) {
620  verify_language_tag (*reel_asset->language(), notes);
621  }
622 
623  if (!reel_asset->entry_point()) {
625  } else if (reel_asset->entry_point().get()) {
627  }
628 }
629 
630 
631 struct State
632 {
633  boost::optional<string> subtitle_language;
634 };
635 
636 
638 void
640  shared_ptr<const SMPTESubtitleAsset> asset,
641  optional<int64_t> reel_asset_duration,
642  vector<VerificationNote>& notes
643  )
644 {
645  if (asset->language()) {
646  verify_language_tag (*asset->language(), notes);
647  } else {
649  }
650 
651  auto const size = boost::filesystem::file_size(asset->file().get());
652  if (size > 115 * 1024 * 1024) {
653  notes.push_back (
655  );
656  }
657 
658  /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
659  * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
660  */
661  auto fonts = asset->font_data ();
662  int total_size = 0;
663  for (auto i: fonts) {
664  total_size += i.second.size();
665  }
666  if (total_size > 10 * 1024 * 1024) {
667  notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
668  }
669 
670  if (!asset->start_time()) {
672  } else if (asset->start_time() != Time()) {
674  }
675 
676  if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
677  notes.push_back (
678  {
681  String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
682  asset->file().get()
683  });
684  }
685 }
686 
687 
689 void
691  shared_ptr<const SMPTESubtitleAsset> asset,
692  vector<VerificationNote>& notes,
693  State& state
694  )
695 {
696  if (asset->language()) {
697  if (!state.subtitle_language) {
698  state.subtitle_language = *asset->language();
699  } else if (state.subtitle_language != *asset->language()) {
701  }
702  }
703 
704  DCP_ASSERT (asset->resource_id());
705  auto xml_id = asset->xml_id();
706  if (xml_id) {
707  if (asset->resource_id().get() != xml_id) {
709  }
710 
711  if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
713  }
714  } else {
715  notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
716  }
717 }
718 
719 
721 static void
723  shared_ptr<const SubtitleAsset> asset,
724  optional<int64_t> reel_asset_duration,
725  function<void (string, optional<boost::filesystem::path>)> stage,
726  boost::filesystem::path xsd_dtd_directory,
727  vector<VerificationNote>& notes,
728  State& state
729  )
730 {
731  stage ("Checking subtitle XML", asset->file());
732  /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
733  * gets passed through libdcp which may clean up and therefore hide errors.
734  */
735  if (asset->raw_xml()) {
736  validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
737  } else {
738  notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
739  }
740 
741  auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
742  if (smpte) {
743  verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
744  verify_smpte_subtitle_asset (smpte, notes, state);
745  }
746 }
747 
748 
750 static void
752  shared_ptr<const SubtitleAsset> asset,
753  optional<int64_t> reel_asset_duration,
754  function<void (string, optional<boost::filesystem::path>)> stage,
755  boost::filesystem::path xsd_dtd_directory,
756  vector<VerificationNote>& notes
757  )
758 {
759  stage ("Checking closed caption XML", asset->file());
760  /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
761  * gets passed through libdcp which may clean up and therefore hide errors.
762  */
763  auto raw_xml = asset->raw_xml();
764  if (raw_xml) {
765  validate_xml (*raw_xml, xsd_dtd_directory, notes);
766  if (raw_xml->size() > 256 * 1024) {
767  notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
768  }
769  } else {
770  notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
771  }
772 
773  auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
774  if (smpte) {
775  verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
776  }
777 }
778 
779 
781 static
782 void
784  vector<shared_ptr<Reel>> reels,
785  int edit_rate,
786  vector<VerificationNote>& notes,
787  std::function<bool (shared_ptr<Reel>)> check,
788  std::function<optional<string> (shared_ptr<Reel>)> xml,
789  std::function<int64_t (shared_ptr<Reel>)> duration
790  )
791 {
792  /* end of last subtitle (in editable units) */
793  optional<int64_t> last_out;
794  auto too_short = false;
795  auto too_close = false;
796  auto too_early = false;
797  auto reel_overlap = false;
798  auto empty_text = false;
799  /* current reel start time (in editable units) */
800  int64_t reel_offset = 0;
801 
802  std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool)> parse;
803  parse = [&parse, &last_out, &too_short, &too_close, &too_early, &empty_text, &reel_offset](cxml::ConstNodePtr node, optional<int> tcr, optional<Time> start_time, int er, bool first_reel) {
804  if (node->name() == "Subtitle") {
805  Time in (node->string_attribute("TimeIn"), tcr);
806  if (start_time) {
807  in -= *start_time;
808  }
809  Time out (node->string_attribute("TimeOut"), tcr);
810  if (start_time) {
811  out -= *start_time;
812  }
813  if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
814  too_early = true;
815  }
816  auto length = out - in;
817  if (length.as_editable_units_ceil(er) < 15) {
818  too_short = true;
819  }
820  if (last_out) {
821  /* XXX: this feels dubious - is it really what Bv2.1 means? */
822  auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
823  if (distance >= 0 && distance < 2) {
824  too_close = true;
825  }
826  }
827  last_out = reel_offset + out.as_editable_units_floor(er);
828  } else if (node->name() == "Text") {
829  std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
830  if (!node->content().empty()) {
831  return true;
832  }
833  for (auto i: node->node_children()) {
834  if (node_has_content(i)) {
835  return true;
836  }
837  }
838  return false;
839  };
840  if (!node_has_content(node)) {
841  empty_text = true;
842  }
843  }
844 
845  for (auto i: node->node_children()) {
846  parse(i, tcr, start_time, er, first_reel);
847  }
848  };
849 
850  for (auto i = 0U; i < reels.size(); ++i) {
851  if (!check(reels[i])) {
852  continue;
853  }
854 
855  auto reel_xml = xml(reels[i]);
856  if (!reel_xml) {
857  notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
858  continue;
859  }
860 
861  /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
862  * read in by libdcp's parser.
863  */
864 
865  shared_ptr<cxml::Document> doc;
866  optional<int> tcr;
867  optional<Time> start_time;
868  try {
869  doc = make_shared<cxml::Document>("SubtitleReel");
870  doc->read_string (*reel_xml);
871  tcr = doc->number_child<int>("TimeCodeRate");
872  auto start_time_string = doc->optional_string_child("StartTime");
873  if (start_time_string) {
874  start_time = Time(*start_time_string, tcr);
875  }
876  } catch (...) {
877  doc = make_shared<cxml::Document>("DCSubtitle");
878  doc->read_string (*reel_xml);
879  }
880  parse (doc, tcr, start_time, edit_rate, i == 0);
881  auto end = reel_offset + duration(reels[i]);
882  if (last_out && *last_out > end) {
883  reel_overlap = true;
884  }
885  reel_offset = end;
886  }
887 
888  if (last_out && *last_out > reel_offset) {
889  reel_overlap = true;
890  }
891 
892  if (too_early) {
893  notes.push_back({
894  VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
895  });
896  }
897 
898  if (too_short) {
899  notes.push_back ({
900  VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
901  });
902  }
903 
904  if (too_close) {
905  notes.push_back ({
906  VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
907  });
908  }
909 
910  if (reel_overlap) {
911  notes.push_back ({
912  VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
913  });
914  }
915 
916  if (empty_text) {
917  notes.push_back ({
918  VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_TEXT
919  });
920  }
921 }
922 
923 
924 static
925 void
926 verify_closed_caption_details (
927  vector<shared_ptr<Reel>> reels,
928  vector<VerificationNote>& notes
929  )
930 {
931  std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
932  find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
933  for (auto i: node->node_children()) {
934  if (i->name() == "Text") {
935  text_or_image.push_back (i);
936  } else {
937  find_text_or_image (i, text_or_image);
938  }
939  }
940  };
941 
942  auto mismatched_valign = false;
943  auto incorrect_order = false;
944 
945  std::function<void (cxml::ConstNodePtr)> parse;
946  parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
947  if (node->name() == "Subtitle") {
948  vector<cxml::ConstNodePtr> text_or_image;
949  find_text_or_image (node, text_or_image);
950  optional<string> last_valign;
951  optional<float> last_vpos;
952  for (auto i: text_or_image) {
953  auto valign = i->optional_string_attribute("VAlign");
954  if (!valign) {
955  valign = i->optional_string_attribute("Valign").get_value_or("center");
956  }
957  auto vpos = i->optional_number_attribute<float>("VPosition");
958  if (!vpos) {
959  vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
960  }
961 
962  if (last_valign) {
963  if (*last_valign != valign) {
964  mismatched_valign = true;
965  }
966  }
967  last_valign = valign;
968 
969  if (!mismatched_valign) {
970  if (last_vpos) {
971  if (*last_valign == "top" || *last_valign == "center") {
972  if (*vpos < *last_vpos) {
973  incorrect_order = true;
974  }
975  } else {
976  if (*vpos > *last_vpos) {
977  incorrect_order = true;
978  }
979  }
980  }
981  last_vpos = vpos;
982  }
983  }
984  }
985 
986  for (auto i: node->node_children()) {
987  parse(i);
988  }
989  };
990 
991  for (auto reel: reels) {
992  for (auto ccap: reel->closed_captions()) {
993  auto reel_xml = ccap->asset()->raw_xml();
994  if (!reel_xml) {
995  notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
996  continue;
997  }
998 
999  /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1000  * read in by libdcp's parser.
1001  */
1002 
1003  shared_ptr<cxml::Document> doc;
1004  optional<int> tcr;
1005  optional<Time> start_time;
1006  try {
1007  doc = make_shared<cxml::Document>("SubtitleReel");
1008  doc->read_string (*reel_xml);
1009  } catch (...) {
1010  doc = make_shared<cxml::Document>("DCSubtitle");
1011  doc->read_string (*reel_xml);
1012  }
1013  parse (doc);
1014  }
1015  }
1016 
1017  if (mismatched_valign) {
1018  notes.push_back ({
1019  VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN,
1020  });
1021  }
1022 
1023  if (incorrect_order) {
1024  notes.push_back ({
1025  VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING,
1026  });
1027  }
1028 }
1029 
1030 
1032 {
1033  bool warning_length_exceeded = false;
1034  bool error_length_exceeded = false;
1035  bool line_count_exceeded = false;
1036 };
1037 
1038 
1039 static
1040 void
1041 verify_text_lines_and_characters (
1042  shared_ptr<SubtitleAsset> asset,
1043  int warning_length,
1044  int error_length,
1045  LinesCharactersResult* result
1046  )
1047 {
1048  class Event
1049  {
1050  public:
1051  Event (Time time_, float position_, int characters_)
1052  : time (time_)
1053  , position (position_)
1054  , characters (characters_)
1055  {}
1056 
1057  Event (Time time_, shared_ptr<Event> start_)
1058  : time (time_)
1059  , start (start_)
1060  {}
1061 
1062  Time time;
1063  int position; //< position from 0 at top of screen to 100 at bottom
1064  int characters;
1065  shared_ptr<Event> start;
1066  };
1067 
1068  vector<shared_ptr<Event>> events;
1069 
1070  auto position = [](shared_ptr<const SubtitleString> sub) {
1071  switch (sub->v_align()) {
1072  case VAlign::TOP:
1073  return lrintf(sub->v_position() * 100);
1074  case VAlign::CENTER:
1075  return lrintf((0.5f + sub->v_position()) * 100);
1076  case VAlign::BOTTOM:
1077  return lrintf((1.0f - sub->v_position()) * 100);
1078  }
1079 
1080  return 0L;
1081  };
1082 
1083  for (auto j: asset->subtitles()) {
1084  auto text = dynamic_pointer_cast<const SubtitleString>(j);
1085  if (text) {
1086  auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1087  events.push_back(in);
1088  events.push_back(make_shared<Event>(text->out(), in));
1089  }
1090  }
1091 
1092  std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1093  return a->time < b->time;
1094  });
1095 
1096  map<int, int> current;
1097  for (auto i: events) {
1098  if (current.size() > 3) {
1099  result->line_count_exceeded = true;
1100  }
1101  for (auto j: current) {
1102  if (j.second > warning_length) {
1103  result->warning_length_exceeded = true;
1104  }
1105  if (j.second > error_length) {
1106  result->error_length_exceeded = true;
1107  }
1108  }
1109 
1110  if (i->start) {
1111  /* end of a subtitle */
1112  DCP_ASSERT (current.find(i->start->position) != current.end());
1113  if (current[i->start->position] == i->start->characters) {
1114  current.erase(i->start->position);
1115  } else {
1116  current[i->start->position] -= i->start->characters;
1117  }
1118  } else {
1119  /* start of a subtitle */
1120  if (current.find(i->position) == current.end()) {
1121  current[i->position] = i->characters;
1122  } else {
1123  current[i->position] += i->characters;
1124  }
1125  }
1126  }
1127 }
1128 
1129 
1130 static
1131 void
1132 verify_text_details (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1133 {
1134  if (reels.empty()) {
1135  return;
1136  }
1137 
1138  if (reels[0]->main_subtitle()) {
1139  verify_text_details (reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1140  [](shared_ptr<Reel> reel) {
1141  return static_cast<bool>(reel->main_subtitle());
1142  },
1143  [](shared_ptr<Reel> reel) {
1144  auto interop = dynamic_pointer_cast<ReelInteropSubtitleAsset>(reel->main_subtitle());
1145  if (interop) {
1146  return interop->asset()->raw_xml();
1147  }
1148  auto smpte = dynamic_pointer_cast<ReelSMPTESubtitleAsset>(reel->main_subtitle());
1149  DCP_ASSERT (smpte);
1150  return smpte->asset()->raw_xml();
1151  },
1152  [](shared_ptr<Reel> reel) {
1153  return reel->main_subtitle()->actual_duration();
1154  }
1155  );
1156  }
1157 
1158  for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1159  verify_text_details (reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1160  [i](shared_ptr<Reel> reel) {
1161  return i < reel->closed_captions().size();
1162  },
1163  [i](shared_ptr<Reel> reel) {
1164  return reel->closed_captions()[i]->asset()->raw_xml();
1165  },
1166  [i](shared_ptr<Reel> reel) {
1167  return reel->closed_captions()[i]->actual_duration();
1168  }
1169  );
1170  }
1171 
1172  verify_closed_caption_details (reels, notes);
1173 }
1174 
1175 
1176 void
1177 verify_extension_metadata (shared_ptr<CPL> cpl, vector<VerificationNote>& notes)
1178 {
1179  DCP_ASSERT (cpl->file());
1180  cxml::Document doc ("CompositionPlaylist");
1181  doc.read_file (cpl->file().get());
1182 
1183  auto missing = false;
1184  string malformed;
1185 
1186  if (auto reel_list = doc.node_child("ReelList")) {
1187  auto reels = reel_list->node_children("Reel");
1188  if (!reels.empty()) {
1189  if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1190  if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1191  if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1192  missing = true;
1193  for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1194  if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1195  continue;
1196  }
1197  missing = false;
1198  if (auto name = extension->optional_node_child("Name")) {
1199  if (name->content() != "Application") {
1200  malformed = "<Name> should be 'Application'";
1201  }
1202  }
1203  if (auto property_list = extension->optional_node_child("PropertyList")) {
1204  if (auto property = property_list->optional_node_child("Property")) {
1205  if (auto name = property->optional_node_child("Name")) {
1206  if (name->content() != "DCP Constraints Profile") {
1207  malformed = "<Name> property should be 'DCP Constraints Profile'";
1208  }
1209  }
1210  if (auto value = property->optional_node_child("Value")) {
1211  if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1212  malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1213  }
1214  }
1215  }
1216  }
1217  }
1218  } else {
1219  missing = true;
1220  }
1221  }
1222  }
1223  }
1224  }
1225 
1226  if (missing) {
1227  notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1228  } else if (!malformed.empty()) {
1229  notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1230  }
1231 }
1232 
1233 
1234 bool
1235 pkl_has_encrypted_assets (shared_ptr<DCP> dcp, shared_ptr<PKL> pkl)
1236 {
1237  vector<string> encrypted;
1238  for (auto i: dcp->cpls()) {
1239  for (auto j: i->reel_file_assets()) {
1240  if (j->asset_ref().resolved()) {
1241  auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1242  if (mxf && mxf->encrypted()) {
1243  encrypted.push_back(j->asset_ref().id());
1244  }
1245  }
1246  }
1247  }
1248 
1249  for (auto i: pkl->asset_list()) {
1250  if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1251  return true;
1252  }
1253  }
1254 
1255  return false;
1256 }
1257 
1258 
1259 vector<VerificationNote>
1260 dcp::verify (
1261  vector<boost::filesystem::path> directories,
1262  function<void (string, optional<boost::filesystem::path>)> stage,
1263  function<void (float)> progress,
1264  optional<boost::filesystem::path> xsd_dtd_directory
1265  )
1266 {
1267  if (!xsd_dtd_directory) {
1268  xsd_dtd_directory = resources_directory() / "xsd";
1269  }
1270  *xsd_dtd_directory = boost::filesystem::canonical (*xsd_dtd_directory);
1271 
1272  vector<VerificationNote> notes;
1273  State state{};
1274 
1275  vector<shared_ptr<DCP>> dcps;
1276  for (auto i: directories) {
1277  dcps.push_back (make_shared<DCP>(i));
1278  }
1279 
1280  for (auto dcp: dcps) {
1281  stage ("Checking DCP", dcp->directory());
1282  bool carry_on = true;
1283  try {
1284  dcp->read (&notes, true);
1285  } catch (MissingAssetmapError& e) {
1286  notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1287  carry_on = false;
1288  } catch (ReadError& e) {
1289  notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1290  } catch (XMLError& e) {
1291  notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1292  } catch (MXFFileError& e) {
1293  notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1294  } catch (cxml::Error& e) {
1295  notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1296  }
1297 
1298  if (!carry_on) {
1299  continue;
1300  }
1301 
1302  if (dcp->standard() != Standard::SMPTE) {
1304  }
1305 
1306  for (auto cpl: dcp->cpls()) {
1307  stage ("Checking CPL", cpl->file());
1308  validate_xml (cpl->file().get(), *xsd_dtd_directory, notes);
1309 
1310  if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1312  }
1313 
1314  for (auto const& i: cpl->additional_subtitle_languages()) {
1315  verify_language_tag (i, notes);
1316  }
1317 
1318  if (cpl->release_territory()) {
1319  if (!cpl->release_territory_scope() || cpl->release_territory_scope().get() != "http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata#scope/release-territory/UNM49") {
1320  auto terr = cpl->release_territory().get();
1321  /* Must be a valid region tag, or "001" */
1322  try {
1323  LanguageTag::RegionSubtag test (terr);
1324  } catch (...) {
1325  if (terr != "001") {
1327  }
1328  }
1329  }
1330  }
1331 
1332  if (dcp->standard() == Standard::SMPTE) {
1333  if (!cpl->annotation_text()) {
1334  notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1335  } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1336  notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1337  }
1338  }
1339 
1340  for (auto i: dcp->pkls()) {
1341  /* Check that the CPL's hash corresponds to the PKL */
1342  optional<string> h = i->hash(cpl->id());
1343  if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1344  notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1345  }
1346 
1347  /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1348  optional<string> required_annotation_text;
1349  for (auto j: i->asset_list()) {
1350  /* See if this is a CPL */
1351  for (auto k: dcp->cpls()) {
1352  if (j->id() == k->id()) {
1353  if (!required_annotation_text) {
1354  /* First CPL we have found; this is the required AnnotationText unless we find another */
1355  required_annotation_text = cpl->content_title_text();
1356  } else {
1357  /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1358  required_annotation_text = boost::none;
1359  }
1360  }
1361  }
1362  }
1363 
1364  if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1366  }
1367  }
1368 
1369  /* set to true if any reel has a MainSubtitle */
1370  auto have_main_subtitle = false;
1371  /* set to true if any reel has no MainSubtitle */
1372  auto have_no_main_subtitle = false;
1373  /* fewest number of closed caption assets seen in a reel */
1374  size_t fewest_closed_captions = SIZE_MAX;
1375  /* most number of closed caption assets seen in a reel */
1376  size_t most_closed_captions = 0;
1377  map<Marker, Time> markers_seen;
1378 
1379  for (auto reel: cpl->reels()) {
1380  stage ("Checking reel", optional<boost::filesystem::path>());
1381 
1382  for (auto i: reel->assets()) {
1383  if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1384  notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1385  }
1386  if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1387  notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1388  }
1389  auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1390  if (i->encryptable() && !file_asset->hash()) {
1392  }
1393  }
1394 
1395  if (dcp->standard() == Standard::SMPTE) {
1396  boost::optional<int64_t> duration;
1397  for (auto i: reel->assets()) {
1398  if (!duration) {
1399  duration = i->actual_duration();
1400  } else if (*duration != i->actual_duration()) {
1402  break;
1403  }
1404  }
1405  }
1406 
1407  if (reel->main_picture()) {
1408  /* Check reel stuff */
1409  auto const frame_rate = reel->main_picture()->frame_rate();
1410  if (frame_rate.denominator != 1 ||
1411  (frame_rate.numerator != 24 &&
1412  frame_rate.numerator != 25 &&
1413  frame_rate.numerator != 30 &&
1414  frame_rate.numerator != 48 &&
1415  frame_rate.numerator != 50 &&
1416  frame_rate.numerator != 60 &&
1417  frame_rate.numerator != 96)) {
1418  notes.push_back ({
1419  VerificationNote::Type::ERROR,
1421  String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1422  });
1423  }
1424  /* Check asset */
1425  if (reel->main_picture()->asset_ref().resolved()) {
1426  verify_main_picture_asset (dcp, reel->main_picture(), stage, progress, notes);
1427  }
1428  }
1429 
1430  if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1431  verify_main_sound_asset (dcp, reel->main_sound(), stage, progress, notes);
1432  }
1433 
1434  if (reel->main_subtitle()) {
1435  verify_main_subtitle_reel (reel->main_subtitle(), notes);
1436  if (reel->main_subtitle()->asset_ref().resolved()) {
1437  verify_subtitle_asset (reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, *xsd_dtd_directory, notes, state);
1438  }
1439  have_main_subtitle = true;
1440  } else {
1441  have_no_main_subtitle = true;
1442  }
1443 
1444  for (auto i: reel->closed_captions()) {
1445  verify_closed_caption_reel (i, notes);
1446  if (i->asset_ref().resolved()) {
1447  verify_closed_caption_asset (i->asset(), i->duration(), stage, *xsd_dtd_directory, notes);
1448  }
1449  }
1450 
1451  if (reel->main_markers()) {
1452  for (auto const& i: reel->main_markers()->get()) {
1453  markers_seen.insert (i);
1454  }
1455  }
1456 
1457  fewest_closed_captions = std::min (fewest_closed_captions, reel->closed_captions().size());
1458  most_closed_captions = std::max (most_closed_captions, reel->closed_captions().size());
1459  }
1460 
1461  verify_text_details (cpl->reels(), notes);
1462 
1463  if (dcp->standard() == Standard::SMPTE) {
1464 
1465  if (have_main_subtitle && have_no_main_subtitle) {
1467  }
1468 
1469  if (fewest_closed_captions != most_closed_captions) {
1471  }
1472 
1473  if (cpl->content_kind() == ContentKind::FEATURE) {
1474  if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1476  }
1477  if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1479  }
1480  }
1481 
1482  auto ffoc = markers_seen.find(Marker::FFOC);
1483  if (ffoc == markers_seen.end()) {
1484  notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1485  } else if (ffoc->second.e != 1) {
1486  notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1487  }
1488 
1489  auto lfoc = markers_seen.find(Marker::LFOC);
1490  if (lfoc == markers_seen.end()) {
1491  notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1492  } else {
1493  auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1494  if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1495  notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1496  }
1497  }
1498 
1499  LinesCharactersResult result;
1500  for (auto reel: cpl->reels()) {
1501  if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1502  verify_text_lines_and_characters (reel->main_subtitle()->asset(), 52, 79, &result);
1503  }
1504  }
1505 
1506  if (result.line_count_exceeded) {
1507  notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1508  }
1509  if (result.error_length_exceeded) {
1510  notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1511  } else if (result.warning_length_exceeded) {
1512  notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1513  }
1514 
1515  result = LinesCharactersResult();
1516  for (auto reel: cpl->reels()) {
1517  for (auto i: reel->closed_captions()) {
1518  if (i->asset()) {
1519  verify_text_lines_and_characters (i->asset(), 32, 32, &result);
1520  }
1521  }
1522  }
1523 
1524  if (result.line_count_exceeded) {
1526  }
1527  if (result.error_length_exceeded) {
1529  }
1530 
1531  if (!cpl->full_content_title_text()) {
1532  /* Since FullContentTitleText is assumed always to exist if there's a CompositionMetadataAsset we
1533  * can use it as a proxy for CompositionMetadataAsset's existence.
1534  */
1535  notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1536  } else if (!cpl->version_number()) {
1538  }
1539 
1540  verify_extension_metadata (cpl, notes);
1541 
1542  if (cpl->any_encrypted()) {
1543  cxml::Document doc ("CompositionPlaylist");
1544  DCP_ASSERT (cpl->file());
1545  doc.read_file (cpl->file().get());
1546  if (!doc.optional_node_child("Signature")) {
1548  }
1549  }
1550  }
1551  }
1552 
1553  for (auto pkl: dcp->pkls()) {
1554  stage ("Checking PKL", pkl->file());
1555  validate_xml (pkl->file().get(), *xsd_dtd_directory, notes);
1556  if (pkl_has_encrypted_assets(dcp, pkl)) {
1557  cxml::Document doc ("PackingList");
1558  doc.read_file (pkl->file().get());
1559  if (!doc.optional_node_child("Signature")) {
1561  }
1562  }
1563  }
1564 
1565  if (dcp->asset_map_path()) {
1566  stage ("Checking ASSETMAP", dcp->asset_map_path().get());
1567  validate_xml (dcp->asset_map_path().get(), *xsd_dtd_directory, notes);
1568  } else {
1569  notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1570  }
1571  }
1572 
1573  return notes;
1574 }
1575 
1576 
1577 string
1579 {
1590  switch (note.code()) {
1592  return *note.note();
1594  return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1596  return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1598  return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1600  return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1602  return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1604  return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1606  return "The asset map contains an empty asset path.";
1608  return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1610  return "The DCP contains both SMPTE and Interop parts.";
1612  return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1614  return "No ASSETMAP or ASSETMAP.xml was found.";
1616  return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1618  return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1620  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());
1622  return String::compose("The instantaneous bit rate of the picture asset %1 is close to the limit of 250Mbit/s in at least one place.", note.file()->filename());
1624  return String::compose("The asset %1 that this DCP refers to is not included in the DCP. It may be a VF.", note.note().get());
1626  return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1628  return "This DCP does not use the SMPTE standard.";
1630  return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1632  return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1634  return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1636  return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1638  return "3D 4K DCPs are not allowed.";
1640  return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1642  return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1644  return String::compose("The size %1 of the fonts in timed text asset %2 is larger than the 10MB maximum.", note.note().get(), note.file()->filename());
1646  return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1648  return "Some subtitle assets have different <Language> tags than others";
1650  return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1652  return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1654  return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1656  return "At least one subtitle lasts less than 15 frames.";
1658  return "At least one pair of subtitles is separated by less than 2 frames.";
1660  return "At least one subtitle extends outside of its reel.";
1662  return "There are more than 3 subtitle lines in at least one place in the DCP.";
1664  return "There are more than 52 characters in at least one subtitle line.";
1666  return "There are more than 79 characters in at least one subtitle line.";
1668  return "There are more than 3 closed caption lines in at least one place.";
1670  return "There are more than 32 characters in at least one closed caption line.";
1672  return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1674  return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1676  return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>", note.note().get());
1678  return "All assets in a reel do not have the same duration.";
1680  return "At least one reel contains a subtitle asset, but some reel(s) do not";
1682  return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1684  return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
1686  return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
1688  return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
1690  return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
1692  return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
1694  return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker";
1696  return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker";
1698  return "There should be a FFOC (first frame of content) marker";
1700  return "There should be a LFOC (last frame of content) marker";
1702  return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
1704  return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
1706  return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
1708  return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
1710  return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
1712  return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
1714  return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
1716  return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
1718  return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
1720  return "Some assets are encrypted but some are not.";
1722  return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1)", note.note().get());
1724  return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
1726  return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
1728  return "The JPEG2000 tile size is not the same as the image size.";
1730  return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
1732  return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
1734  return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
1736  return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
1738  return String::compose("Incorrect POC marker content found (%1)", note.note().get());
1740  return "POC marker found outside main header";
1742  return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
1744  return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
1746  return "No TLM marker was found in a JPEG2000 codestream.";
1748  return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
1750  return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
1752  {
1753  vector<string> parts;
1754  boost::split (parts, note.note().get(), boost::is_any_of(" "));
1755  DCP_ASSERT (parts.size() == 2);
1756  return String::compose("The reel duration of some timed text (%1) is not the same as the ContainerDuration of its MXF (%2).", parts[0], parts[1]);
1757  }
1759  return "Some aspect of this DCP could not be checked because it is encrypted.";
1761  return "There is an empty <Text> node in a subtitle or closed caption.";
1763  return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
1765  return "Some closed captions are not listed in the order of their vertical position.";
1766  }
1767 
1768  return "";
1769 }
1770 
1771 
1772 bool
1773 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
1774 {
1775  return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
1776 }
1777 
1778 
1779 bool
1780 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
1781 {
1782  if (a.type() != b.type()) {
1783  return a.type() < b.type();
1784  }
1785 
1786  if (a.code() != b.code()) {
1787  return a.code() < b.code();
1788  }
1789 
1790  if (a.note() != b.note()) {
1791  return a.note().get_value_or("") < b.note().get_value_or("");
1792  }
1793 
1794  if (a.file() != b.file()) {
1795  return a.file().get_value_or("") < b.file().get_value_or("");
1796  }
1797 
1798  return a.line().get_value_or(0) < b.line().get_value_or(0);
1799 }
1800 
1801 
1802 std::ostream&
1803 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
1804 {
1805  s << note_to_string (note);
1806  if (note.note()) {
1807  s << " [" << note.note().get() << "]";
1808  }
1809  if (note.file()) {
1810  s << " [" << note.file().get() << "]";
1811  }
1812  if (note.line()) {
1813  s << " [" << note.line().get() << "]";
1814  }
1815  return s;
1816 }
1817 
Class to hold an arbitrary block of data.
Definition: array_data.h:55
A fraction (i.e. a thing with an integer numerator and an integer denominator).
Definition: types.h:214
An exception related to an MXF file.
Definition: exceptions.h:82
A miscellaneous exception.
Definition: exceptions.h:94
Thrown when no ASSETMAP was found when trying to read a DCP.
Definition: exceptions.h:154
Any error that occurs when reading data from a DCP.
Definition: exceptions.h:106
A representation of time within a DCP.
Definition: dcp_time.h:73
int64_t as_editable_units_ceil(int tcr_) const
Definition: dcp_time.cc:350
int64_t as_editable_units_floor(int tcr_) const
Definition: dcp_time.cc:343
@ BV21_ERROR
may not always be considered an error, but violates a "shall" requirement of Bv2.1
An XML error.
Definition: exceptions.h:164
CPL class.
DCP class.
Exceptions thrown by libdcp.
InteropSubtitleAsset class.
MonoPictureFrame class.
Namespace for everything in libdcp.
Definition: array_data.h:50
@ FFEC
first frame of end credits
@ FFMC
first frame of moving credits
@ LFOC
last frame of composition
@ FFOC
first frame of composition
std::string note_to_string(dcp::VerificationNote note)
Definition: verify.cc:1578
@ BOTTOM
vertical position is distance from bottom of screen to bottom of subtitle
@ TOP
vertical position is distance from top of screen to top of subtitle
@ CENTER
vertical position is distance from centre of screen to centre of subtitle
void verify_j2k(std::shared_ptr< const Data > data, std::vector< VerificationNote > &notes)
Definition: verify_j2k.cc:68
std::string make_digest(boost::filesystem::path filename, boost::function< void(float)>)
Methods for conversion to/from string.
ReelClosedCaptionAsset class.
ReelInteropSubtitleAsset class.
ReelPictureAsset class.
ReelSoundAsset class.
ReelSubtitleAsset class.
SMPTESubtitleAsset class.
StereoPictureAsset class.
StereoPictureFrame class.
Definition: verify.cc:632
The integer, two-dimensional size of something.
Definition: types.h:71
void verify_smpte_subtitle_asset(shared_ptr< const SMPTESubtitleAsset > asset, vector< VerificationNote > &notes, State &state)
Definition: verify.cc:690
static void verify_subtitle_asset(shared_ptr< const SubtitleAsset > asset, optional< int64_t > reel_asset_duration, function< void(string, optional< boost::filesystem::path >)> stage, boost::filesystem::path xsd_dtd_directory, vector< VerificationNote > &notes, State &state)
Definition: verify.cc:722
static void verify_closed_caption_asset(shared_ptr< const SubtitleAsset > asset, optional< int64_t > reel_asset_duration, function< void(string, optional< boost::filesystem::path >)> stage, boost::filesystem::path xsd_dtd_directory, vector< VerificationNote > &notes)
Definition: verify.cc:751
static void verify_text_details(vector< shared_ptr< Reel >> reels, int edit_rate, vector< VerificationNote > &notes, std::function< bool(shared_ptr< Reel >)> check, std::function< optional< string >(shared_ptr< Reel >)> xml, std::function< int64_t(shared_ptr< Reel >)> duration)
Definition: verify.cc:783
void verify_smpte_timed_text_asset(shared_ptr< const SMPTESubtitleAsset > asset, optional< int64_t > reel_asset_duration, vector< VerificationNote > &notes)
Definition: verify.cc:639
dcp::verify() method and associated code
Verification that JPEG2000 files meet requirements.