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 "filesystem.h"
45 #include "interop_text_asset.h"
46 #include "mono_j2k_picture_asset.h"
47 #include "mono_j2k_picture_frame.h"
48 #include "raw_convert.h"
49 #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_text_asset.h"
55 #include "reel_text_asset.h"
56 #include "smpte_text_asset.h"
59 #include "verify.h"
60 #include "verify_internal.h"
61 #include "verify_j2k.h"
62 #include <libxml/parserInternals.h>
63 #include <xercesc/dom/DOMAttr.hpp>
64 #include <xercesc/dom/DOMDocument.hpp>
65 #include <xercesc/dom/DOMError.hpp>
66 #include <xercesc/dom/DOMErrorHandler.hpp>
67 #include <xercesc/dom/DOMException.hpp>
68 #include <xercesc/dom/DOMImplementation.hpp>
69 #include <xercesc/dom/DOMImplementationLS.hpp>
70 #include <xercesc/dom/DOMImplementationRegistry.hpp>
71 #include <xercesc/dom/DOMLSParser.hpp>
72 #include <xercesc/dom/DOMLocator.hpp>
73 #include <xercesc/dom/DOMNamedNodeMap.hpp>
74 #include <xercesc/dom/DOMNodeList.hpp>
75 #include <xercesc/framework/LocalFileInputSource.hpp>
76 #include <xercesc/framework/MemBufInputSource.hpp>
77 #include <xercesc/parsers/AbstractDOMParser.hpp>
78 #include <xercesc/parsers/XercesDOMParser.hpp>
79 #include <xercesc/sax/HandlerBase.hpp>
80 #include <xercesc/util/PlatformUtils.hpp>
81 #include <boost/algorithm/string.hpp>
82 #include <iostream>
83 #include <map>
84 #include <numeric>
85 #include <regex>
86 #include <set>
87 #include <vector>
88 
89 
90 using std::cout;
91 using std::dynamic_pointer_cast;
92 using std::function;
93 using std::list;
94 using std::make_shared;
95 using std::map;
96 using std::max;
97 using std::set;
98 using std::shared_ptr;
99 using std::string;
100 using std::vector;
101 using boost::optional;
102 
103 
104 using namespace dcp;
105 using namespace xercesc;
106 
107 
108 static
109 string
110 xml_ch_to_string (XMLCh const * a)
111 {
112  char* x = XMLString::transcode(a);
113  string const o(x);
114  XMLString::release(&x);
115  return o;
116 }
117 
118 
120 {
121 public:
122  XMLValidationError (SAXParseException const & e)
123  : _message (xml_ch_to_string(e.getMessage()))
124  , _line (e.getLineNumber())
125  , _column (e.getColumnNumber())
126  , _public_id (e.getPublicId() ? xml_ch_to_string(e.getPublicId()) : "")
127  , _system_id (e.getSystemId() ? xml_ch_to_string(e.getSystemId()) : "")
128  {
129 
130  }
131 
132  string message () const {
133  return _message;
134  }
135 
136  uint64_t line () const {
137  return _line;
138  }
139 
140  uint64_t column () const {
141  return _column;
142  }
143 
144  string public_id () const {
145  return _public_id;
146  }
147 
148  string system_id () const {
149  return _system_id;
150  }
151 
152 private:
153  string _message;
154  uint64_t _line;
155  uint64_t _column;
156  string _public_id;
157  string _system_id;
158 };
159 
160 
161 class DCPErrorHandler : public ErrorHandler
162 {
163 public:
164  void warning(const SAXParseException& e) override
165  {
166  maybe_add (XMLValidationError(e));
167  }
168 
169  void error(const SAXParseException& e) override
170  {
171  maybe_add (XMLValidationError(e));
172  }
173 
174  void fatalError(const SAXParseException& e) override
175  {
176  maybe_add (XMLValidationError(e));
177  }
178 
179  void resetErrors() override {
180  _errors.clear ();
181  }
182 
183  list<XMLValidationError> errors () const {
184  return _errors;
185  }
186 
187 private:
188  void maybe_add (XMLValidationError e)
189  {
190  /* XXX: nasty hack */
191  if (
192  e.message().find("schema document") != string::npos &&
193  e.message().find("has different target namespace from the one specified in instance document") != string::npos
194  ) {
195  return;
196  }
197 
198  _errors.push_back (e);
199  }
200 
201  list<XMLValidationError> _errors;
202 };
203 
204 
206 {
207 public:
208  StringToXMLCh (string a)
209  {
210  _buffer = XMLString::transcode(a.c_str());
211  }
212 
213  StringToXMLCh (StringToXMLCh const&) = delete;
214  StringToXMLCh& operator= (StringToXMLCh const&) = delete;
215 
216  ~StringToXMLCh ()
217  {
218  XMLString::release (&_buffer);
219  }
220 
221  XMLCh const * get () const {
222  return _buffer;
223  }
224 
225 private:
226  XMLCh* _buffer;
227 };
228 
229 
230 class LocalFileResolver : public EntityResolver
231 {
232 public:
233  LocalFileResolver (boost::filesystem::path xsd_dtd_directory)
234  : _xsd_dtd_directory (xsd_dtd_directory)
235  {
236  /* XXX: I'm not clear on what things need to be in this list; some XSDs are apparently, magically
237  * found without being here.
238  */
239  add("http://www.w3.org/2001/XMLSchema.dtd", "XMLSchema.dtd");
240  add("http://www.w3.org/2001/03/xml.xsd", "xml.xsd");
241  add("http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd", "xmldsig-core-schema.xsd");
242  add("http://www.digicine.com/schemas/437-Y/2007/Main-Stereo-Picture-CPL.xsd", "Main-Stereo-Picture-CPL.xsd");
243  add("http://www.digicine.com/PROTO-ASDCP-CPL-20040511.xsd", "PROTO-ASDCP-CPL-20040511.xsd");
244  add("http://www.digicine.com/PROTO-ASDCP-PKL-20040311.xsd", "PROTO-ASDCP-PKL-20040311.xsd");
245  add("http://www.digicine.com/PROTO-ASDCP-AM-20040311.xsd", "PROTO-ASDCP-AM-20040311.xsd");
246  add("http://www.digicine.com/PROTO-ASDCP-CC-CPL-20070926#", "PROTO-ASDCP-CC-CPL-20070926.xsd");
247  add("interop-subs", "DCSubtitle.v1.mattsson.xsd");
248  add("http://www.smpte-ra.org/schemas/428-7/2010/DCST.xsd", "DCDMSubtitle-2010.xsd");
249  add("http://www.smpte-ra.org/schemas/428-7/2014/DCST.xsd", "DCDMSubtitle-2014.xsd");
250  add("http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata", "SMPTE-429-16.xsd");
251  add("http://www.dolby.com/schemas/2012/AD", "Dolby-2012-AD.xsd");
252  add("http://www.smpte-ra.org/schemas/429-10/2008/Main-Stereo-Picture-CPL", "SMPTE-429-10-2008.xsd");
253  }
254 
255  InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id) override
256  {
257  if (!system_id) {
258  return 0;
259  }
260  auto system_id_str = xml_ch_to_string (system_id);
261  auto p = _xsd_dtd_directory;
262  if (_files.find(system_id_str) == _files.end()) {
263  p /= system_id_str;
264  } else {
265  p /= _files[system_id_str];
266  }
267  StringToXMLCh ch (p.string());
268  return new LocalFileInputSource(ch.get());
269  }
270 
271 private:
272  void add (string uri, string file)
273  {
274  _files[uri] = file;
275  }
276 
277  std::map<string, string> _files;
278  boost::filesystem::path _xsd_dtd_directory;
279 };
280 
281 
282 static void
283 parse (XercesDOMParser& parser, boost::filesystem::path xml)
284 {
285  parser.parse(xml.c_str());
286 }
287 
288 
289 static void
290 parse (XercesDOMParser& parser, string xml)
291 {
292  xercesc::MemBufInputSource buf(reinterpret_cast<unsigned char const*>(xml.c_str()), xml.size(), "");
293  parser.parse(buf);
294 }
295 
296 
297 template <class T>
298 void
299 validate_xml(Context& context, T xml)
300 {
301  try {
302  XMLPlatformUtils::Initialize ();
303  } catch (XMLException& e) {
304  throw MiscError ("Failed to initialise xerces library");
305  }
306 
307  DCPErrorHandler error_handler;
308 
309  /* All the xerces objects in this scope must be destroyed before XMLPlatformUtils::Terminate() is called */
310  {
311  XercesDOMParser parser;
312  parser.setValidationScheme(XercesDOMParser::Val_Always);
313  parser.setDoNamespaces(true);
314  parser.setDoSchema(true);
315 
316  vector<string> schema;
317  schema.push_back("xml.xsd");
318  schema.push_back("xmldsig-core-schema.xsd");
319  schema.push_back("SMPTE-429-7-2006-CPL.xsd");
320  schema.push_back("SMPTE-429-8-2006-PKL.xsd");
321  schema.push_back("SMPTE-429-9-2007-AM.xsd");
322  schema.push_back("Main-Stereo-Picture-CPL.xsd");
323  schema.push_back("PROTO-ASDCP-CPL-20040511.xsd");
324  schema.push_back("PROTO-ASDCP-PKL-20040311.xsd");
325  schema.push_back("PROTO-ASDCP-AM-20040311.xsd");
326  schema.push_back("DCSubtitle.v1.mattsson.xsd");
327  schema.push_back("DCDMSubtitle-2010.xsd");
328  schema.push_back("DCDMSubtitle-2014.xsd");
329  schema.push_back("PROTO-ASDCP-CC-CPL-20070926.xsd");
330  schema.push_back("SMPTE-429-16.xsd");
331  schema.push_back("Dolby-2012-AD.xsd");
332  schema.push_back("SMPTE-429-10-2008.xsd");
333  schema.push_back("xlink.xsd");
334  schema.push_back("SMPTE-335-2012.xsd");
335  schema.push_back("SMPTE-395-2014-13-1-aaf.xsd");
336  schema.push_back("isdcf-mca.xsd");
337  schema.push_back("SMPTE-429-12-2008.xsd");
338 
339  /* XXX: I'm not especially clear what this is for, but it seems to be necessary.
340  * Schemas that are not mentioned in this list are not read, and the things
341  * they describe are not checked.
342  */
343  string locations;
344  for (auto i: schema) {
345  locations += String::compose("%1 %1 ", i, i);
346  }
347 
348  parser.setExternalSchemaLocation(locations.c_str());
349  parser.setValidationSchemaFullChecking(true);
350  parser.setErrorHandler(&error_handler);
351 
352  LocalFileResolver resolver(context.xsd_dtd_directory);
353  parser.setEntityResolver(&resolver);
354 
355  try {
356  parser.resetDocumentPool();
357  parse(parser, xml);
358  } catch (XMLException& e) {
359  throw MiscError(xml_ch_to_string(e.getMessage()));
360  } catch (DOMException& e) {
361  throw MiscError(xml_ch_to_string(e.getMessage()));
362  } catch (...) {
363  throw MiscError("Unknown exception from xerces");
364  }
365  }
366 
367  XMLPlatformUtils::Terminate ();
368 
369  for (auto i: error_handler.errors()) {
370  context.error(
372  i.message(),
373  boost::trim_copy(i.public_id() + " " + i.system_id()),
374  i.line()
375  );
376  }
377 }
378 
379 
380 enum class VerifyAssetResult {
381  GOOD,
382  CPL_PKL_DIFFER,
383  BAD
384 };
385 
386 
387 static VerifyAssetResult
388 verify_asset(
389  Context& context,
390  shared_ptr<const ReelFileAsset> reel_file_asset,
391  string* reference_hash,
392  string* calculated_hash
393  )
394 {
395  DCP_ASSERT(reference_hash);
396  DCP_ASSERT(calculated_hash);
397 
398  /* When reading the DCP the hash will have been set to the one from the PKL/CPL.
399  * We want to calculate the hash of the actual file contents here, so that we
400  * can check it. unset_hash() means that this calculation will happen on the
401  * call to hash().
402  */
403  reel_file_asset->asset_ref()->unset_hash();
404  *calculated_hash = reel_file_asset->asset_ref()->hash([&context](int64_t done, int64_t total) {
405  context.progress(float(done) / total);
406  });
407 
408  auto pkls = context.dcp->pkls();
409  /* We've read this DCP in so it must have at least one PKL */
410  DCP_ASSERT (!pkls.empty());
411 
412  auto asset = reel_file_asset->asset_ref().asset();
413 
414  optional<string> maybe_pkl_hash;
415  for (auto i: pkls) {
416  maybe_pkl_hash = i->hash (reel_file_asset->asset_ref()->id());
417  if (maybe_pkl_hash) {
418  break;
419  }
420  }
421 
422  DCP_ASSERT(maybe_pkl_hash);
423  *reference_hash = *maybe_pkl_hash;
424 
425  auto cpl_hash = reel_file_asset->hash();
426  if (cpl_hash && *cpl_hash != *reference_hash) {
427  return VerifyAssetResult::CPL_PKL_DIFFER;
428  }
429 
430  if (*calculated_hash != *reference_hash) {
431  return VerifyAssetResult::BAD;
432  }
433 
434  return VerifyAssetResult::GOOD;
435 }
436 
437 
438 static void
439 verify_language_tag(Context& context, string tag)
440 {
441  try {
442  LanguageTag test (tag);
443  } catch (LanguageTagError &) {
444  context.bv21_error(VerificationNote::Code::INVALID_LANGUAGE, tag);
445  }
446 }
447 
448 
449 static void
450 verify_picture_asset(
451  Context& context,
452  shared_ptr<const ReelFileAsset> reel_file_asset,
453  boost::filesystem::path file,
454  int64_t start_frame
455  )
456 {
457  auto asset = dynamic_pointer_cast<J2KPictureAsset>(reel_file_asset->asset_ref().asset());
458  auto const duration = asset->intrinsic_duration ();
459 
460  auto check_and_add = [&context](vector<VerificationNote> const& j2k_notes) {
461  for (auto i: j2k_notes) {
462  context.add_note_if_not_existing(i);
463  }
464  };
465 
466  int const max_frame = rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
467  int const risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
468 
469  bool any_bad_frames_seen = false;
470 
471  auto check_frame_size = [max_frame, risky_frame, file, start_frame, &any_bad_frames_seen](Context& context, int index, int size, int frame_rate) {
472  if (size > max_frame) {
473  context.add_note(
475  VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
476  ).set_frame(start_frame + index).set_frame_rate(frame_rate)
477  );
478  any_bad_frames_seen = true;
479  } else if (size > risky_frame) {
480  context.add_note(
482  VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
483  ).set_frame(start_frame + index).set_frame_rate(frame_rate)
484  );
485  any_bad_frames_seen = true;
486  }
487  };
488 
489  if (auto mono_asset = dynamic_pointer_cast<MonoJ2KPictureAsset>(reel_file_asset->asset_ref().asset())) {
490  auto reader = mono_asset->start_read ();
491  for (int64_t i = 0; i < duration; ++i) {
492  auto frame = reader->get_frame (i);
493  check_frame_size(context, i, frame->size(), mono_asset->frame_rate().numerator);
494  if (!mono_asset->encrypted() || mono_asset->key()) {
495  vector<VerificationNote> j2k_notes;
496  verify_j2k(frame, start_frame, i, mono_asset->frame_rate().numerator, j2k_notes);
497  check_and_add (j2k_notes);
498  }
499  context.progress(float(i) / duration);
500  }
501  } else if (auto stereo_asset = dynamic_pointer_cast<StereoJ2KPictureAsset>(asset)) {
502  auto reader = stereo_asset->start_read ();
503  for (int64_t i = 0; i < duration; ++i) {
504  auto frame = reader->get_frame (i);
505  check_frame_size(context, i, frame->left()->size(), stereo_asset->frame_rate().numerator);
506  check_frame_size(context, i, frame->right()->size(), stereo_asset->frame_rate().numerator);
507  if (!stereo_asset->encrypted() || stereo_asset->key()) {
508  vector<VerificationNote> j2k_notes;
509  verify_j2k(frame->left(), start_frame, i, stereo_asset->frame_rate().numerator, j2k_notes);
510  verify_j2k(frame->right(), start_frame, i, stereo_asset->frame_rate().numerator, j2k_notes);
511  check_and_add (j2k_notes);
512  }
513  context.progress(float(i) / duration);
514  }
515 
516  }
517 
518  if (!any_bad_frames_seen) {
520  }
521 }
522 
523 
524 static void
525 verify_main_picture_asset(Context& context, shared_ptr<const ReelPictureAsset> reel_asset, int64_t start_frame)
526 {
527  auto asset = reel_asset->asset();
528  auto const file = *asset->file();
529 
530  if (context.options.check_asset_hashes && (!context.options.maximum_asset_size_for_hash_check || filesystem::file_size(file) < *context.options.maximum_asset_size_for_hash_check)) {
531  context.stage("Checking picture asset hash", file);
532  string reference_hash;
533  string calculated_hash;
534  auto const r = verify_asset(context, reel_asset, &reference_hash, &calculated_hash);
535  switch (r) {
536  case VerifyAssetResult::BAD:
537  context.add_note(
539  VerificationNote::Type::ERROR,
541  file
542  ).set_reference_hash(reference_hash).set_calculated_hash(calculated_hash)
543  );
544  break;
545  case VerifyAssetResult::CPL_PKL_DIFFER:
547  break;
548  default:
550  break;
551  }
552  }
553 
554  context.stage("Checking picture frame sizes", asset->file());
555  verify_picture_asset(context, reel_asset, file, start_frame);
556 
557  /* Only flat/scope allowed by Bv2.1 */
558  if (
559  asset->size() != Size(2048, 858) &&
560  asset->size() != Size(1998, 1080) &&
561  asset->size() != Size(4096, 1716) &&
562  asset->size() != Size(3996, 2160)) {
563  context.bv21_error(VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS, String::compose("%1x%2", asset->size().width, asset->size().height), file);
564  }
565 
566  /* Only 24, 25, 48fps allowed for 2K */
567  if (
568  (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
569  (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
570  ) {
571  context.bv21_error(
573  String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
574  file
575  );
576  }
577 
578  if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
579  /* Only 24fps allowed for 4K */
580  if (asset->edit_rate() != Fraction(24, 1)) {
581  context.bv21_error(
583  String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
584  file
585  );
586  }
587 
588  /* Only 2D allowed for 4K */
589  if (dynamic_pointer_cast<const StereoJ2KPictureAsset>(asset)) {
590  context.bv21_error(
592  String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
593  file
594  );
595 
596  }
597  }
598 }
599 
600 
601 static void
602 verify_main_sound_asset(Context& context, shared_ptr<const ReelSoundAsset> reel_asset)
603 {
604  auto asset = reel_asset->asset();
605  auto const file = *asset->file();
606 
607  if (context.options.check_asset_hashes && (!context.options.maximum_asset_size_for_hash_check || filesystem::file_size(file) < *context.options.maximum_asset_size_for_hash_check)) {
608  context.stage("Checking sound asset hash", file);
609  string reference_hash;
610  string calculated_hash;
611  auto const r = verify_asset(context, reel_asset, &reference_hash, &calculated_hash);
612  switch (r) {
613  case VerifyAssetResult::BAD:
614  context.add_note(
616  VerificationNote::Type::ERROR,
618  file
619  ).set_reference_hash(reference_hash).set_calculated_hash(calculated_hash)
620  );
621  break;
622  case VerifyAssetResult::CPL_PKL_DIFFER:
624  break;
625  default:
626  break;
627  }
628  }
629 
630  if (!context.audio_channels) {
631  context.audio_channels = asset->channels();
632  } else if (*context.audio_channels != asset->channels()) {
634  }
635 
636  context.stage("Checking sound asset metadata", file);
637 
638  if (auto lang = asset->language()) {
639  verify_language_tag(context, *lang);
640  }
641  if (asset->sampling_rate() != 48000) {
642  context.bv21_error(VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), file);
643  }
644  if (asset->bit_depth() != 24) {
645  context.error(VerificationNote::Code::INVALID_SOUND_BIT_DEPTH, raw_convert<string>(asset->bit_depth()), file);
646  }
647 }
648 
649 
650 static void
651 verify_main_subtitle_reel(Context& context, shared_ptr<const ReelTextAsset> reel_asset)
652 {
653  /* XXX: is Language compulsory? */
654  if (reel_asset->language()) {
655  verify_language_tag(context, *reel_asset->language());
656  }
657 
658  if (!reel_asset->entry_point()) {
659  context.bv21_error(VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id());
660  } else if (reel_asset->entry_point().get()) {
661  context.bv21_error(VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id());
662  }
663 }
664 
665 
666 static void
667 verify_closed_caption_reel(Context& context, shared_ptr<const ReelTextAsset> reel_asset)
668 {
669  /* XXX: is Language compulsory? */
670  if (reel_asset->language()) {
671  verify_language_tag(context, *reel_asset->language());
672  }
673 
674  if (!reel_asset->entry_point()) {
675  context.bv21_error(VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id());
676  } else if (reel_asset->entry_point().get()) {
677  context.bv21_error(VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id());
678  }
679 }
680 
681 
683 void
685  Context& context,
686  shared_ptr<const SMPTETextAsset> asset,
687  optional<int64_t> reel_asset_duration
688  )
689 {
690  if (asset->language()) {
691  verify_language_tag(context, *asset->language());
692  } else {
693  context.bv21_error(VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file());
694  }
695 
696  auto const size = filesystem::file_size(asset->file().get());
697  if (size > 115 * 1024 * 1024) {
698  context.bv21_error(VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file());
699  }
700 
701  /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
702  * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
703  */
704  auto fonts = asset->font_data ();
705  int total_size = 0;
706  for (auto i: fonts) {
707  total_size += i.second.size();
708  }
709  if (total_size > 10 * 1024 * 1024) {
710  context.bv21_error(VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get());
711  }
712 
713  if (!asset->start_time()) {
714  context.bv21_error(VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get());
715  } else if (asset->start_time() != Time()) {
716  context.bv21_error(VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get());
717  }
718 
719  if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
720  context.bv21_error(
722  String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
723  asset->file().get()
724  );
725  }
726 }
727 
728 
730 void
731 verify_interop_text_asset(Context& context, shared_ptr<const InteropTextAsset> asset)
732 {
733  if (asset->texts().empty()) {
734  context.error(VerificationNote::Code::MISSING_SUBTITLE, asset->id(), asset->file().get());
735  }
736  auto const unresolved = asset->unresolved_fonts();
737  if (!unresolved.empty()) {
738  context.error(VerificationNote::Code::MISSING_FONT, unresolved.front());
739  }
740 }
741 
742 
744 void
745 verify_smpte_subtitle_asset(Context& context, shared_ptr<const SMPTETextAsset> asset)
746 {
747  if (asset->language()) {
748  if (!context.subtitle_language) {
749  context.subtitle_language = *asset->language();
750  } else if (context.subtitle_language != *asset->language()) {
752  }
753  }
754 
755  DCP_ASSERT (asset->resource_id());
756  auto xml_id = asset->xml_id();
757  if (xml_id) {
758  if (asset->resource_id().get() != xml_id) {
760  }
761 
762  if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
764  }
765  } else {
767  }
768 
769  if (asset->raw_xml()) {
770  /* Deluxe require this in their QC even if it seems never to be mentioned in any standard */
771  cxml::Document doc("SubtitleReel");
772  doc.read_string(*asset->raw_xml());
773  auto issue_date = doc.string_child("IssueDate");
774  std::regex reg("^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d$");
775  if (!std::regex_match(issue_date, reg)) {
776  context.warning(VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, issue_date);
777  }
778  }
779 }
780 
781 
783 static void
784 verify_subtitle_asset(Context& context, shared_ptr<const TextAsset> asset, optional<int64_t> reel_asset_duration)
785 {
786  context.stage("Checking subtitle XML", asset->file());
787  /* Note: we must not use TextAsset::xml_as_string() here as that will mean the data on disk
788  * gets passed through libdcp which may clean up and therefore hide errors.
789  */
790  if (asset->raw_xml()) {
791  validate_xml(context, asset->raw_xml().get());
792  } else {
794  }
795 
796  auto namespace_count = [](shared_ptr<const TextAsset> asset, string root_node) {
797  cxml::Document doc(root_node);
798  doc.read_string(asset->raw_xml().get());
799  auto root = dynamic_cast<xmlpp::Element*>(doc.node())->cobj();
800  int count = 0;
801  for (auto ns = root->nsDef; ns != nullptr; ns = ns->next) {
802  ++count;
803  }
804  return count;
805  };
806 
807  auto interop = dynamic_pointer_cast<const InteropTextAsset>(asset);
808  if (interop) {
809  verify_interop_text_asset(context, interop);
810  if (namespace_count(asset, "DCSubtitle") > 1) {
812  }
813  }
814 
815  auto smpte = dynamic_pointer_cast<const SMPTETextAsset>(asset);
816  if (smpte) {
817  verify_smpte_timed_text_asset(context, smpte, reel_asset_duration);
818  verify_smpte_subtitle_asset(context, smpte);
819  /* This asset may be encrypted and in that case we'll have no raw_xml() */
820  if (asset->raw_xml() && namespace_count(asset, "SubtitleReel") > 1) {
822  }
823  }
824 }
825 
826 
828 static void
830  Context& context,
831  shared_ptr<const TextAsset> asset,
832  optional<int64_t> reel_asset_duration
833  )
834 {
835  context.stage("Checking closed caption XML", asset->file());
836  /* Note: we must not use TextAsset::xml_as_string() here as that will mean the data on disk
837  * gets passed through libdcp which may clean up and therefore hide errors.
838  */
839  auto raw_xml = asset->raw_xml();
840  if (raw_xml) {
841  validate_xml(context, *raw_xml);
842  if (raw_xml->size() > 256 * 1024) {
843  context.bv21_error(VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file());
844  }
845  } else {
847  }
848 
849  auto interop = dynamic_pointer_cast<const InteropTextAsset>(asset);
850  if (interop) {
851  verify_interop_text_asset(context, interop);
852  }
853 
854  auto smpte = dynamic_pointer_cast<const SMPTETextAsset>(asset);
855  if (smpte) {
856  verify_smpte_timed_text_asset(context, smpte, reel_asset_duration);
857  }
858 }
859 
860 
862 static
863 void
865  Context& context,
866  vector<shared_ptr<Reel>> reels,
867  int edit_rate,
868  std::function<bool (shared_ptr<Reel>)> check,
869  std::function<optional<string> (shared_ptr<Reel>)> xml,
870  std::function<int64_t (shared_ptr<Reel>)> duration,
871  std::function<std::string (shared_ptr<Reel>)> id
872  )
873 {
874  /* end of last subtitle (in editable units) */
875  optional<int64_t> last_out;
876  auto too_short = false;
877  auto too_close = false;
878  auto too_early = false;
879  auto reel_overlap = false;
880  auto empty_text = false;
881  /* current reel start time (in editable units) */
882  int64_t reel_offset = 0;
883  optional<string> missing_load_font_id;
884 
885  std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool, bool&, vector<string>&)> parse;
886 
887  parse = [&parse, &last_out, &too_short, &too_close, &too_early, &empty_text, &reel_offset, &missing_load_font_id](
888  cxml::ConstNodePtr node,
889  optional<int> tcr,
890  optional<Time> start_time,
891  int er,
892  bool first_reel,
893  bool& has_text,
894  vector<string>& font_ids
895  ) {
896  if (node->name() == "Subtitle") {
897  Time in (node->string_attribute("TimeIn"), tcr);
898  if (start_time) {
899  in -= *start_time;
900  }
901  Time out (node->string_attribute("TimeOut"), tcr);
902  if (start_time) {
903  out -= *start_time;
904  }
905  if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
906  too_early = true;
907  }
908  auto length = out - in;
909  if (length.as_editable_units_ceil(er) < 15) {
910  too_short = true;
911  }
912  if (last_out) {
913  /* XXX: this feels dubious - is it really what Bv2.1 means? */
914  auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
915  if (distance >= 0 && distance < 2) {
916  too_close = true;
917  }
918  }
919  last_out = reel_offset + out.as_editable_units_floor(er);
920  } else if (node->name() == "Text") {
921  std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
922  if (!node->content().empty()) {
923  return true;
924  }
925  for (auto i: node->node_children()) {
926  if (node_has_content(i)) {
927  return true;
928  }
929  }
930  return false;
931  };
932  if (!node_has_content(node)) {
933  empty_text = true;
934  }
935  has_text = true;
936  } else if (node->name() == "LoadFont") {
937  if (auto const id = node->optional_string_attribute("Id")) {
938  font_ids.push_back(*id);
939  } else if (auto const id = node->optional_string_attribute("ID")) {
940  font_ids.push_back(*id);
941  }
942  } else if (node->name() == "Font") {
943  if (auto const font_id = node->optional_string_attribute("Id")) {
944  if (std::find_if(font_ids.begin(), font_ids.end(), [font_id](string const& id) { return id == font_id; }) == font_ids.end()) {
945  missing_load_font_id = font_id;
946  }
947  }
948  }
949  for (auto i: node->node_children()) {
950  parse(i, tcr, start_time, er, first_reel, has_text, font_ids);
951  }
952  };
953 
954  for (auto i = 0U; i < reels.size(); ++i) {
955  if (!check(reels[i])) {
956  continue;
957  }
958 
959  auto reel_xml = xml(reels[i]);
960  if (!reel_xml) {
962  continue;
963  }
964 
965  /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
966  * read in by libdcp's parser.
967  */
968 
969  shared_ptr<cxml::Document> doc;
970  optional<int> tcr;
971  optional<Time> start_time;
972  switch (context.dcp->standard().get_value_or(dcp::Standard::SMPTE)) {
973  case dcp::Standard::INTEROP:
974  doc = make_shared<cxml::Document>("DCSubtitle");
975  doc->read_string (*reel_xml);
976  break;
977  case dcp::Standard::SMPTE:
978  doc = make_shared<cxml::Document>("SubtitleReel");
979  doc->read_string (*reel_xml);
980  tcr = doc->number_child<int>("TimeCodeRate");
981  if (auto start_time_string = doc->optional_string_child("StartTime")) {
982  start_time = Time(*start_time_string, tcr);
983  }
984  break;
985  }
986  bool has_text = false;
987  vector<string> font_ids;
988  parse(doc, tcr, start_time, edit_rate, i == 0, has_text, font_ids);
989  auto end = reel_offset + duration(reels[i]);
990  if (last_out && *last_out > end) {
991  reel_overlap = true;
992  }
993  reel_offset = end;
994 
995  if (context.dcp->standard() && *context.dcp->standard() == dcp::Standard::SMPTE && has_text && font_ids.empty()) {
996  context.add_note(dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT).set_id(id(reels[i])));
997  }
998  }
999 
1000  if (last_out && *last_out > reel_offset) {
1001  reel_overlap = true;
1002  }
1003 
1004  if (too_early) {
1006  }
1007 
1008  if (too_short) {
1010  }
1011 
1012  if (too_close) {
1014  }
1015 
1016  if (reel_overlap) {
1018  }
1019 
1020  if (empty_text) {
1021  context.warning(VerificationNote::Code::EMPTY_TEXT);
1022  }
1023 
1024  if (missing_load_font_id) {
1025  context.add_note(dcp::VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT).set_id(*missing_load_font_id));
1026  }
1027 }
1028 
1029 
1030 static
1031 void
1032 verify_closed_caption_details(Context& context, vector<shared_ptr<Reel>> reels)
1033 {
1034  std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
1035  find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
1036  for (auto i: node->node_children()) {
1037  if (i->name() == "Text") {
1038  text_or_image.push_back (i);
1039  } else {
1040  find_text_or_image (i, text_or_image);
1041  }
1042  }
1043  };
1044 
1045  auto mismatched_valign = false;
1046  auto incorrect_order = false;
1047 
1048  std::function<void (cxml::ConstNodePtr)> parse;
1049  parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
1050  if (node->name() == "Subtitle") {
1051  vector<cxml::ConstNodePtr> text_or_image;
1052  find_text_or_image (node, text_or_image);
1053  optional<string> last_valign;
1054  optional<float> last_vpos;
1055  for (auto i: text_or_image) {
1056  auto valign = i->optional_string_attribute("VAlign");
1057  if (!valign) {
1058  valign = i->optional_string_attribute("Valign").get_value_or("center");
1059  }
1060  auto vpos = i->optional_number_attribute<float>("VPosition");
1061  if (!vpos) {
1062  vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
1063  }
1064 
1065  if (last_valign) {
1066  if (*last_valign != valign) {
1067  mismatched_valign = true;
1068  }
1069  }
1070  last_valign = valign;
1071 
1072  if (!mismatched_valign) {
1073  if (last_vpos) {
1074  if (*last_valign == "top" || *last_valign == "center") {
1075  if (*vpos < *last_vpos) {
1076  incorrect_order = true;
1077  }
1078  } else {
1079  if (*vpos > *last_vpos) {
1080  incorrect_order = true;
1081  }
1082  }
1083  }
1084  last_vpos = vpos;
1085  }
1086  }
1087  }
1088 
1089  for (auto i: node->node_children()) {
1090  parse(i);
1091  }
1092  };
1093 
1094  for (auto reel: reels) {
1095  for (auto ccap: reel->closed_captions()) {
1096  auto reel_xml = ccap->asset()->raw_xml();
1097  if (!reel_xml) {
1099  continue;
1100  }
1101 
1102  /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1103  * read in by libdcp's parser.
1104  */
1105 
1106  shared_ptr<cxml::Document> doc;
1107  optional<int> tcr;
1108  optional<Time> start_time;
1109  try {
1110  doc = make_shared<cxml::Document>("SubtitleReel");
1111  doc->read_string (*reel_xml);
1112  } catch (...) {
1113  doc = make_shared<cxml::Document>("DCSubtitle");
1114  doc->read_string (*reel_xml);
1115  }
1116  parse (doc);
1117  }
1118  }
1119 
1120  if (mismatched_valign) {
1122  }
1123 
1124  if (incorrect_order) {
1126  }
1127 }
1128 
1129 
1130 void
1132  shared_ptr<const TextAsset> asset,
1133  int warning_length,
1134  int error_length,
1135  LinesCharactersResult* result
1136  )
1137 {
1138  class Event
1139  {
1140  public:
1141  Event (Time time_, float position_, int characters_)
1142  : time (time_)
1143  , position (position_)
1144  , characters (characters_)
1145  {}
1146 
1147  Event (Time time_, shared_ptr<Event> start_)
1148  : time (time_)
1149  , start (start_)
1150  {}
1151 
1152  Time time;
1153  int position = 0;
1154  int characters = 0;
1155  shared_ptr<Event> start;
1156  };
1157 
1158  vector<shared_ptr<Event>> events;
1159 
1160  auto position = [](shared_ptr<const TextString> sub) {
1161  switch (sub->v_align()) {
1162  case VAlign::TOP:
1163  return lrintf(sub->v_position() * 100);
1164  case VAlign::CENTER:
1165  return lrintf((0.5f + sub->v_position()) * 100);
1166  case VAlign::BOTTOM:
1167  return lrintf((1.0f - sub->v_position()) * 100);
1168  }
1169 
1170  return 0L;
1171  };
1172 
1173  /* Make a list of "subtitle starts" and "subtitle ends" events */
1174  for (auto j: asset->texts()) {
1175  auto text = dynamic_pointer_cast<const TextString>(j);
1176  if (text) {
1177  auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1178  events.push_back(in);
1179  events.push_back(make_shared<Event>(text->out(), in));
1180  }
1181  }
1182 
1183  std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event> const& b) {
1184  return a->time < b->time;
1185  });
1186 
1187  /* The number of characters currently displayed at different vertical positions, i.e. on
1188  * what we consider different lines. Key is the vertical position (0 to 100) and the value
1189  * is a list of the active subtitles in that position.
1190  */
1191  map<int, vector<shared_ptr<Event>>> current;
1192  for (auto i: events) {
1193  if (current.size() > 3) {
1194  result->line_count_exceeded = true;
1195  }
1196  for (auto j: current) {
1197  int length = std::accumulate(j.second.begin(), j.second.end(), 0, [](int total, shared_ptr<const Event> event) { return total + event->characters; });
1198  if (length > warning_length) {
1199  result->warning_length_exceeded = true;
1200  }
1201  if (length > error_length) {
1202  result->error_length_exceeded = true;
1203  }
1204  }
1205 
1206  if (i->start) {
1207  /* end of a subtitle */
1208  DCP_ASSERT (current.find(i->start->position) != current.end());
1209  auto current_position = current[i->start->position];
1210  auto iter = std::find(current_position.begin(), current_position.end(), i->start);
1211  if (iter != current_position.end()) {
1212  current_position.erase(iter);
1213  }
1214  if (current_position.empty()) {
1215  current.erase(i->start->position);
1216  }
1217  } else {
1218  /* start of a subtitle */
1219  if (current.find(i->position) == current.end()) {
1220  current[i->position] = vector<shared_ptr<Event>>{i};
1221  } else {
1222  current[i->position].push_back(i);
1223  }
1224  }
1225  }
1226 }
1227 
1228 
1229 static
1230 void
1231 verify_text_details(Context& context, vector<shared_ptr<Reel>> reels)
1232 {
1233  if (reels.empty()) {
1234  return;
1235  }
1236 
1237  if (reels[0]->main_subtitle() && reels[0]->main_subtitle()->asset_ref().resolved()) {
1238  verify_text_details(context, reels, reels[0]->main_subtitle()->edit_rate().numerator,
1239  [](shared_ptr<Reel> reel) {
1240  return static_cast<bool>(reel->main_subtitle());
1241  },
1242  [](shared_ptr<Reel> reel) {
1243  return reel->main_subtitle()->asset()->raw_xml();
1244  },
1245  [](shared_ptr<Reel> reel) {
1246  return reel->main_subtitle()->actual_duration();
1247  },
1248  [](shared_ptr<Reel> reel) {
1249  return reel->main_subtitle()->id();
1250  }
1251  );
1252  }
1253 
1254  for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1255  verify_text_details(context, reels, reels[0]->closed_captions()[i]->edit_rate().numerator,
1256  [i](shared_ptr<Reel> reel) {
1257  return i < reel->closed_captions().size();
1258  },
1259  [i](shared_ptr<Reel> reel) {
1260  return reel->closed_captions()[i]->asset()->raw_xml();
1261  },
1262  [i](shared_ptr<Reel> reel) {
1263  return reel->closed_captions()[i]->actual_duration();
1264  },
1265  [i](shared_ptr<Reel> reel) {
1266  return reel->closed_captions()[i]->id();
1267  }
1268  );
1269  }
1270 
1271  verify_closed_caption_details(context, reels);
1272 }
1273 
1274 
1275 void
1276 dcp::verify_extension_metadata(Context& context)
1277 {
1278  DCP_ASSERT(context.cpl->file());
1279  cxml::Document doc ("CompositionPlaylist");
1280  doc.read_file(dcp::filesystem::fix_long_path(context.cpl->file().get()));
1281 
1282  auto missing = false;
1283  string malformed;
1284 
1285  if (auto reel_list = doc.node_child("ReelList")) {
1286  auto reels = reel_list->node_children("Reel");
1287  if (!reels.empty()) {
1288  if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1289  if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1290  if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1291  missing = true;
1292  for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1293  if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1294  continue;
1295  }
1296  missing = false;
1297  if (auto name = extension->optional_node_child("Name")) {
1298  if (name->content() != "Application") {
1299  malformed = "<Name> should be 'Application'";
1300  }
1301  }
1302  if (auto property_list = extension->optional_node_child("PropertyList")) {
1303  auto properties = property_list->node_children("Property");
1304  auto is_bv21 = [](shared_ptr<const cxml::Node> property) {
1305  auto name = property->optional_node_child("Name");
1306  auto value = property->optional_node_child("Value");
1307  return name && value && name->content() == "DCP Constraints Profile" && value->content() == "SMPTE-RDD-52:2020-Bv2.1";
1308  };
1309  if (!std::any_of(properties.begin(), properties.end(), is_bv21)) {
1310  malformed = "No correctly-formed DCP Constraints Profile found";
1311  }
1312  }
1313  }
1314  } else {
1315  missing = true;
1316  }
1317  }
1318  }
1319  }
1320  }
1321 
1322  if (missing) {
1323  context.bv21_error(VerificationNote::Code::MISSING_EXTENSION_METADATA, context.cpl->file().get());
1324  } else if (!malformed.empty()) {
1325  context.bv21_error(VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, context.cpl->file().get());
1326  }
1327 }
1328 
1329 
1330 bool
1331 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1332 {
1333  vector<string> encrypted;
1334  for (auto i: dcp->cpls()) {
1335  for (auto j: i->reel_file_assets()) {
1336  if (j->asset_ref().resolved()) {
1337  auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1338  if (mxf && mxf->encrypted()) {
1339  encrypted.push_back(j->asset_ref().id());
1340  }
1341  }
1342  }
1343  }
1344 
1345  for (auto i: pkl->assets()) {
1346  if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1347  return true;
1348  }
1349  }
1350 
1351  return false;
1352 }
1353 
1354 
1355 static
1356 void
1357 verify_reel(
1358  Context& context,
1359  shared_ptr<const Reel> reel,
1360  int64_t start_frame,
1361  optional<dcp::Size> main_picture_active_area,
1362  bool* have_main_subtitle,
1363  bool* have_no_main_subtitle,
1364  size_t* most_closed_captions,
1365  size_t* fewest_closed_captions,
1366  map<Marker, Time>* markers_seen
1367  )
1368 {
1369  for (auto i: reel->assets()) {
1370  if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1371  context.error(VerificationNote::Code::INVALID_DURATION, i->id());
1372  }
1373  if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1375  }
1376  auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1377  if (i->encryptable() && !file_asset->hash()) {
1378  context.bv21_error(VerificationNote::Code::MISSING_HASH, i->id());
1379  }
1380  }
1381 
1382  if (context.dcp->standard() == Standard::SMPTE) {
1383  boost::optional<int64_t> duration;
1384  for (auto i: reel->assets()) {
1385  if (!duration) {
1386  duration = i->actual_duration();
1387  } else if (*duration != i->actual_duration()) {
1389  break;
1390  }
1391  }
1392  }
1393 
1394  if (reel->main_picture()) {
1395  /* Check reel stuff */
1396  auto const frame_rate = reel->main_picture()->frame_rate();
1397  if (frame_rate.denominator != 1 ||
1398  (frame_rate.numerator != 24 &&
1399  frame_rate.numerator != 25 &&
1400  frame_rate.numerator != 30 &&
1401  frame_rate.numerator != 48 &&
1402  frame_rate.numerator != 50 &&
1403  frame_rate.numerator != 60 &&
1404  frame_rate.numerator != 96)) {
1405  context.error(VerificationNote::Code::INVALID_PICTURE_FRAME_RATE, String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator));
1406  }
1407  /* Check asset */
1408  if (reel->main_picture()->asset_ref().resolved()) {
1409  verify_main_picture_asset(context, reel->main_picture(), start_frame);
1410  auto const asset_size = reel->main_picture()->asset()->size();
1411  if (main_picture_active_area) {
1412  if (main_picture_active_area->width > asset_size.width) {
1413  context.error(
1415  String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1416  context.cpl->file().get()
1417  );
1418  }
1419  if (main_picture_active_area->height > asset_size.height) {
1420  context.error(
1422  String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1423  context.cpl->file().get()
1424  );
1425  }
1426  }
1427  }
1428 
1429  }
1430 
1431  if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1432  verify_main_sound_asset(context, reel->main_sound());
1433  }
1434 
1435  if (reel->main_subtitle()) {
1436  verify_main_subtitle_reel(context, reel->main_subtitle());
1437  if (reel->main_subtitle()->asset_ref().resolved()) {
1438  verify_subtitle_asset(context, reel->main_subtitle()->asset(), reel->main_subtitle()->duration());
1439  }
1440  *have_main_subtitle = true;
1441  } else {
1442  *have_no_main_subtitle = true;
1443  }
1444 
1445  for (auto i: reel->closed_captions()) {
1446  verify_closed_caption_reel(context, i);
1447  if (i->asset_ref().resolved()) {
1448  verify_closed_caption_asset(context, i->asset(), i->duration());
1449  }
1450  }
1451 
1452  if (reel->main_markers()) {
1453  for (auto const& i: reel->main_markers()->get()) {
1454  markers_seen->insert(i);
1455  }
1456  if (reel->main_markers()->entry_point()) {
1458  }
1459  if (reel->main_markers()->duration()) {
1461  }
1462  }
1463 
1464  *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1465  *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1466 
1467 }
1468 
1469 
1470 static
1471 void
1472 verify_cpl(Context& context, shared_ptr<const CPL> cpl)
1473 {
1474  context.stage("Checking CPL", cpl->file());
1475  validate_xml(context, cpl->file().get());
1476 
1477  if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1478  context.bv21_error(VerificationNote::Code::PARTIALLY_ENCRYPTED);
1479  } else if (cpl->all_encrypted()) {
1481  } else if (!cpl->all_encrypted()) {
1483  }
1484 
1485  for (auto const& i: cpl->additional_subtitle_languages()) {
1486  verify_language_tag(context, i);
1487  }
1488 
1489  if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1490  /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1491  * of the approved ones.
1492  */
1493  auto all = ContentKind::all();
1494  auto name = cpl->content_kind().name();
1495  transform(name.begin(), name.end(), name.begin(), ::tolower);
1496  auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1497  if (iter == all.end()) {
1498  context.error(VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name());
1499  } else {
1500  context.ok(VerificationNote::Code::VALID_CONTENT_KIND, cpl->content_kind().name());
1501  }
1502  }
1503 
1504  if (cpl->release_territory()) {
1505  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") {
1506  auto terr = cpl->release_territory().get();
1507  bool valid = true;
1508  /* Must be a valid region tag, or "001" */
1509  try {
1510  LanguageTag::RegionSubtag test(terr);
1511  } catch (...) {
1512  if (terr != "001") {
1513  context.bv21_error(VerificationNote::Code::INVALID_LANGUAGE, terr);
1514  valid = false;
1515  }
1516  }
1517  if (valid) {
1519  }
1520  }
1521  }
1522 
1523  for (auto version: cpl->content_versions()) {
1524  if (version.label_text.empty()) {
1525  context.warning(VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT, cpl->file().get());
1526  break;
1527  } else {
1528  context.ok(VerificationNote::Code::VALID_CONTENT_VERSION_LABEL_TEXT, version.label_text);
1529  }
1530  }
1531 
1532  if (context.dcp->standard() == Standard::SMPTE) {
1533  if (!cpl->annotation_text()) {
1534  context.bv21_error(VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->file().get());
1535  } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1536  context.warning(VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->file().get());
1537  } else {
1538  context.ok(VerificationNote::Code::VALID_CPL_ANNOTATION_TEXT, cpl->annotation_text().get());
1539  }
1540  }
1541 
1542  for (auto i: context.dcp->pkls()) {
1543  /* Check that the CPL's hash corresponds to the PKL */
1544  optional<string> h = i->hash(cpl->id());
1545  auto calculated_cpl_hash = make_digest(ArrayData(*cpl->file()));
1546  if (h && calculated_cpl_hash != *h) {
1547  context.add_note(
1549  VerificationNote::Type::ERROR,
1551  cpl->file().get()
1552  ).set_calculated_hash(calculated_cpl_hash).set_reference_hash(*h)
1553  );
1554  } else {
1556  }
1557 
1558  /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1559  optional<string> required_annotation_text;
1560  for (auto j: i->assets()) {
1561  /* See if this is a CPL */
1562  for (auto k: context.dcp->cpls()) {
1563  if (j->id() == k->id()) {
1564  if (!required_annotation_text) {
1565  /* First CPL we have found; this is the required AnnotationText unless we find another */
1566  required_annotation_text = cpl->content_title_text();
1567  } else {
1568  /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1569  required_annotation_text = boost::none;
1570  }
1571  }
1572  }
1573  }
1574 
1575  if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1576  context.bv21_error(VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get());
1577  } else {
1579  }
1580  }
1581 
1582  /* set to true if any reel has a MainSubtitle */
1583  auto have_main_subtitle = false;
1584  /* set to true if any reel has no MainSubtitle */
1585  auto have_no_main_subtitle = false;
1586  /* fewest number of closed caption assets seen in a reel */
1587  size_t fewest_closed_captions = SIZE_MAX;
1588  /* most number of closed caption assets seen in a reel */
1589  size_t most_closed_captions = 0;
1590  map<Marker, Time> markers_seen;
1591 
1592  auto const main_picture_active_area = cpl->main_picture_active_area();
1593  bool active_area_ok = true;
1594  if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1595  context.error(
1597  String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1598  cpl->file().get()
1599  );
1600  active_area_ok = false;
1601  }
1602  if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1603  context.error(
1605  String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1606  cpl->file().get()
1607  );
1608  active_area_ok = false;
1609  }
1610 
1611  if (main_picture_active_area && active_area_ok) {
1612  context.ok(
1613  VerificationNote::Code::VALID_MAIN_PICTURE_ACTIVE_AREA, String::compose("%1x%2", main_picture_active_area->width, main_picture_active_area->height),
1614  cpl->file().get()
1615  );
1616  }
1617 
1618  int64_t frame = 0;
1619  for (auto reel: cpl->reels()) {
1620  context.stage("Checking reel", optional<boost::filesystem::path>());
1621  verify_reel(
1622  context,
1623  reel,
1624  frame,
1625  main_picture_active_area,
1626  &have_main_subtitle,
1627  &have_no_main_subtitle,
1628  &most_closed_captions,
1629  &fewest_closed_captions,
1630  &markers_seen
1631  );
1632  frame += reel->duration();
1633  }
1634 
1635  verify_text_details(context, cpl->reels());
1636 
1637  if (context.dcp->standard() == Standard::SMPTE) {
1638  if (auto msc = cpl->main_sound_configuration()) {
1639  if (context.audio_channels && msc->channels() != *context.audio_channels) {
1640  context.error(
1642  String::compose("MainSoundConfiguration has %1 channels but sound assets have %2", msc->channels(), *context.audio_channels),
1643  cpl->file().get()
1644  );
1645  }
1646  }
1647 
1648  if (have_main_subtitle && have_no_main_subtitle) {
1650  }
1651 
1652  if (fewest_closed_captions != most_closed_captions) {
1654  }
1655 
1656  if (cpl->content_kind() == ContentKind::FEATURE) {
1657  if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1659  }
1660  if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1662  }
1663  }
1664 
1665  auto ffoc = markers_seen.find(Marker::FFOC);
1666  if (ffoc == markers_seen.end()) {
1667  context.warning(VerificationNote::Code::MISSING_FFOC);
1668  } else if (ffoc->second.e != 1) {
1669  context.warning(VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e));
1670  }
1671 
1672  auto lfoc = markers_seen.find(Marker::LFOC);
1673  if (lfoc == markers_seen.end()) {
1674  context.warning(VerificationNote::Code::MISSING_LFOC);
1675  } else {
1676  auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1677  if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1678  context.warning(VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time));
1679  }
1680  }
1681 
1682  LinesCharactersResult result;
1683  for (auto reel: cpl->reels()) {
1684  if (reel->main_subtitle() && reel->main_subtitle()->asset_ref().resolved()) {
1685  verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1686  }
1687  }
1688 
1689  if (result.line_count_exceeded) {
1691  }
1692  if (result.error_length_exceeded) {
1694  } else if (result.warning_length_exceeded) {
1696  }
1697 
1698  result = LinesCharactersResult();
1699  for (auto reel: cpl->reels()) {
1700  for (auto i: reel->closed_captions()) {
1701  if (i->asset()) {
1702  verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1703  }
1704  }
1705  }
1706 
1707  if (result.line_count_exceeded) {
1709  }
1710  if (result.error_length_exceeded) {
1712  }
1713 
1714  if (!cpl->read_composition_metadata()) {
1715  context.bv21_error(VerificationNote::Code::MISSING_CPL_METADATA, cpl->file().get());
1716  } else if (!cpl->version_number()) {
1717  context.bv21_error(VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->file().get());
1718  }
1719 
1720  verify_extension_metadata(context);
1721 
1722  if (cpl->any_encrypted()) {
1723  cxml::Document doc("CompositionPlaylist");
1724  DCP_ASSERT(cpl->file());
1725  doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1726  if (!doc.optional_node_child("Signature")) {
1727  context.bv21_error(VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->file().get());
1728  }
1729  }
1730  }
1731 }
1732 
1733 
1734 static
1735 void
1736 verify_pkl(Context& context, shared_ptr<const PKL> pkl)
1737 {
1738  validate_xml(context, pkl->file().get());
1739 
1740  if (pkl_has_encrypted_assets(context.dcp, pkl)) {
1741  cxml::Document doc("PackingList");
1742  doc.read_file(dcp::filesystem::fix_long_path(pkl->file().get()));
1743  if (!doc.optional_node_child("Signature")) {
1744  context.bv21_error(VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get());
1745  }
1746  }
1747 
1748  set<string> uuid_set;
1749  for (auto asset: pkl->assets()) {
1750  if (!uuid_set.insert(asset->id()).second) {
1751  context.error(VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get());
1752  break;
1753  }
1754  }
1755 }
1756 
1757 
1758 
1759 static
1760 void
1761 verify_assetmap(Context& context, shared_ptr<const DCP> dcp)
1762 {
1763  auto asset_map = dcp->asset_map();
1764  DCP_ASSERT(asset_map);
1765 
1766  validate_xml(context, asset_map->file().get());
1767 
1768  set<string> uuid_set;
1769  for (auto const& asset: asset_map->assets()) {
1770  if (!uuid_set.insert(asset.id()).second) {
1771  context.error(VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get());
1772  break;
1773  }
1774  }
1775 }
1776 
1777 
1779 dcp::verify (
1780  vector<boost::filesystem::path> directories,
1781  vector<dcp::DecryptedKDM> kdms,
1782  function<void (string, optional<boost::filesystem::path>)> stage,
1783  function<void (float)> progress,
1784  VerificationOptions options,
1785  optional<boost::filesystem::path> xsd_dtd_directory
1786  )
1787 {
1788  if (!xsd_dtd_directory) {
1789  xsd_dtd_directory = resources_directory() / "xsd";
1790  }
1791  *xsd_dtd_directory = filesystem::canonical(*xsd_dtd_directory);
1792 
1793  vector<VerificationNote> notes;
1794  Context context(notes, *xsd_dtd_directory, stage, progress, options);
1795 
1796  vector<shared_ptr<DCP>> dcps;
1797  for (auto i: directories) {
1798  dcps.push_back (make_shared<DCP>(i));
1799  }
1800 
1801  for (auto dcp: dcps) {
1802  stage ("Checking DCP", dcp->directory());
1803 
1804  context.dcp = dcp;
1805 
1806  bool carry_on = true;
1807  try {
1808  dcp->read (&notes, true);
1809  } catch (MissingAssetmapError& e) {
1810  context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1811  carry_on = false;
1812  } catch (ReadError& e) {
1813  context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1814  } catch (XMLError& e) {
1815  context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1816  } catch (MXFFileError& e) {
1817  context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1818  } catch (BadURNUUIDError& e) {
1819  context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1820  } catch (cxml::Error& e) {
1821  context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1822  } catch (xmlpp::parse_error& e) {
1823  carry_on = false;
1824  context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1825  }
1826 
1827  if (!carry_on) {
1828  continue;
1829  }
1830 
1831  if (dcp->standard() != Standard::SMPTE) {
1833  }
1834 
1835  for (auto kdm: kdms) {
1836  dcp->add(kdm);
1837  }
1838 
1839  for (auto cpl: dcp->cpls()) {
1840  try {
1841  context.cpl = cpl;
1842  verify_cpl(context, cpl);
1843  context.cpl.reset();
1844  } catch (ReadError& e) {
1845  notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1846  }
1847  }
1848 
1849  for (auto pkl: dcp->pkls()) {
1850  stage("Checking PKL", pkl->file());
1851  verify_pkl(context, pkl);
1852  }
1853 
1854  if (dcp->asset_map_file()) {
1855  stage("Checking ASSETMAP", dcp->asset_map_file().get());
1856  verify_assetmap(context, dcp);
1857  } else {
1859  }
1860  }
1861 
1862  return { notes, dcps };
1863 }
1864 
1865 
1866 string
1867 dcp::note_to_string(VerificationNote note, function<string (string)> process_string, function<string (string)> process_filename)
1868 {
1880  auto filename = [note, process_filename]() {
1881  return process_filename(note.file()->filename().string());
1882  };
1883 
1884 #define compose(format, ...) String::compose(process_string(format), __VA_ARGS__)
1885 
1886  switch (note.code()) {
1888  return process_string(*note.note());
1890  return process_string("The hash of the CPL in the PKL matches the CPL file.");
1892  return compose("The hash (%1) of the CPL (%2) in the PKL does not agree with the CPL file (%3).", note.reference_hash().get(), note.cpl_id().get(), note.calculated_hash().get());
1894  return compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1896  return compose("The hash (%1) of the picture asset %2 does not agree with the PKL file (%3).", note.calculated_hash().get(), filename(), note.reference_hash().get());
1898  return compose("The picture asset %1 has the expected hashes in the CPL and PKL.", filename());
1900  return compose("The PKL and CPL hashes differ for the picture asset %1.", filename());
1902  return compose("The hash (%1) of the sound asset %2 does not agree with the PKL file (%3).", note.calculated_hash().get(), filename(), note.reference_hash().get());
1904  return compose("The PKL and CPL hashes differ for the sound asset %1.", filename());
1906  return process_string("The asset map contains an empty asset path.");
1908  return compose("The file %1 for an asset in the asset map cannot be found.", filename());
1910  return process_string("The DCP contains both SMPTE and Interop parts.");
1912  return compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), filename(), note.line().get());
1914  return process_string("No valid ASSETMAP or ASSETMAP.xml was found.");
1916  return compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1918  return compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1920  return compose("Each frame of the picture asset %1 has a bit rate safely under the limit of 250Mbit/s.", filename());
1922  return compose(
1923  "Frame %1 (timecode %2) in asset %3 has an instantaneous bit rate that is larger than the limit of 250Mbit/s.",
1924  note.frame().get(),
1925  dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
1926  filename()
1927  );
1929  return compose(
1930  "Frame %1 (timecode %2) in asset %3 has an instantaneous bit rate that is close to the limit of 250Mbit/s.",
1931  note.frame().get(),
1932  dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
1933  filename()
1934  );
1936  return compose("The asset %1 that this DCP refers to is not included in the DCP. It may be a VF.", note.note().get());
1938  return compose("The asset %1 is 3D but its MXF is marked as 2D.", filename());
1940  return "This DCP does not use the SMPTE standard.";
1942  return compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1944  return compose("Valid release territory %1.", note.note().get());
1946  return compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), filename());
1948  return compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), filename());
1950  return compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), filename());
1952  return process_string("3D 4K DCPs are not allowed.");
1954  return compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), filename());
1956  return compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), filename());
1958  return compose("The size %1 of the fonts in timed text asset %2 is larger than the 10MB maximum.", note.note().get(), filename());
1960  return compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", filename());
1962  return process_string("Some subtitle assets have different <Language> tags than others");
1964  return compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", filename());
1966  return compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", filename());
1968  return process_string("The first subtitle or closed caption is less than 4 seconds from the start of the DCP.");
1970  return process_string("At least one subtitle lasts less than 15 frames.");
1972  return process_string("At least one pair of subtitles is separated by less than 2 frames.");
1974  return process_string("At least one subtitle extends outside of its reel.");
1976  return process_string("There are more than 3 subtitle lines in at least one place in the DCP.");
1978  return process_string("There are more than 52 characters in at least one subtitle line.");
1980  return process_string("There are more than 79 characters in at least one subtitle line.");
1982  return process_string("There are more than 3 closed caption lines in at least one place.");
1984  return process_string("There are more than 32 characters in at least one closed caption line.");
1986  return compose("The sound asset %1 has a sampling rate of %2", filename(), note.note().get());
1988  return compose("The sound asset %1 has a bit depth of %2", filename(), note.note().get());
1990  return compose("The CPL %1 has no <AnnotationText> tag.", note.cpl_id().get());
1992  return compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.cpl_id().get());
1994  return compose("Valid CPL annotation text %1", note.note().get());
1996  return process_string("All assets in a reel do not have the same duration.");
1998  return process_string("At least one reel contains a subtitle asset, but some reel(s) do not.");
2000  return process_string("At least one reel has closed captions, but reels have different numbers of closed caption assets.");
2002  return compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
2004  return compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
2006  return compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
2008  return compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
2010  return compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
2012  return process_string("The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.");
2014  return process_string("The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.");
2016  return process_string("There should be a FFOC (first frame of content) marker.");
2018  return process_string("There should be a LFOC (last frame of content) marker.");
2020  return compose("The FFOC marker is %1 instead of 1", note.note().get());
2022  return compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
2024  return compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.cpl_id().get());
2026  return compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.cpl_id().get());
2028  return compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.cpl_id().get());
2030  return compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", filename(), note.note().get());
2032  return compose("The CPL %1, which has encrypted content, is not signed.", note.cpl_id().get());
2034  return compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
2036  return compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
2038  return process_string("The PKL and CPL annotation texts match.");
2040  return process_string("All the assets are encrypted.");
2042  return process_string("All the assets are unencrypted.");
2044  return process_string("Some assets are encrypted but some are not.");
2046  return compose(
2047  "Frame %1 (timecode %2) has an invalid JPEG2000 codestream (%3).",
2048  note.frame().get(),
2049  dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
2050  note.note().get()
2051  );
2053  return compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
2055  return compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
2057  return process_string("The JPEG2000 tile size is not the same as the image size.");
2059  return compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
2061  return compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
2063  return compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
2065  return compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
2067  return compose("Incorrect POC marker content found (%1).", note.note().get());
2069  return process_string("POC marker found outside main header.");
2071  return compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
2073  return compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
2075  return process_string("No TLM marker was found in a JPEG2000 codestream.");
2077  return process_string("The Resource ID in a timed text MXF did not match the ID of the contained XML.");
2079  return process_string("The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.");
2081  {
2082  vector<string> parts;
2083  boost::split (parts, note.note().get(), boost::is_any_of(" "));
2084  DCP_ASSERT (parts.size() == 2);
2085  return compose("The reel duration of some timed text (%1) is not the same as the ContainerDuration of its MXF (%2).", parts[0], parts[1]);
2086  }
2088  return process_string("Some aspect of this DCP could not be checked because it is encrypted.");
2090  return process_string("There is an empty <Text> node in a subtitle or closed caption.");
2092  return process_string("Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.");
2094  return process_string("Some closed captions are not listed in the order of their vertical position.");
2096  return process_string("There is an <EntryPoint> node inside a <MainMarkers>.");
2098  return process_string("There is an <Duration> node inside a <MainMarkers>.");
2100  return compose("<ContentKind> has an invalid value %1.", note.note().get());
2102  return compose("Valid <ContentKind> %1.", note.note().get());
2104  return compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
2106  return compose("<MainPictureActiveaArea> %1 is valid", note.note().get());
2108  return compose("The PKL %1 has more than one asset with the same ID.", note.note().get());
2110  return compose("The ASSETMAP %1 has more than one asset with the same ID.", note.note().get());
2112  return compose("The subtitle asset %1 has no subtitles.", note.note().get());
2114  return compose("<IssueDate> has an invalid value: %1", note.note().get());
2116  return compose("The sound assets do not all have the same channel count; the first to differ is %1", filename());
2118  return compose("<MainSoundConfiguration> has an invalid value: %1", note.note().get());
2120  return compose("The font file for font ID \"%1\" was not found, or was not referred to in the ASSETMAP.", note.note().get());
2122  return compose(
2123  "Frame %1 has an image component that is too large (component %2 is %3 bytes in size).",
2124  note.frame().get(), note.component().get(), note.size().get()
2125  );
2127  return compose("The XML in the subtitle asset %1 has more than one namespace declaration.", note.note().get());
2129  return compose("A subtitle or closed caption refers to a font with ID %1 that does not have a corresponding <LoadFont> node", note.id().get());
2131  return compose("The SMPTE subtitle asset %1 has <Text> nodes but no <LoadFont> node", note.id().get());
2133  return compose("The asset with ID %1 in the asset map actually has an id of %2", note.id().get(), note.other_id().get());
2135  return compose("The <LabelText> in a <ContentVersion> in CPL %1 is empty", note.cpl_id().get());
2137  return compose("CPL has valid <ContentVersion> %1", note.note().get());
2139  return compose("The namespace %1 in CPL %2 is invalid", note.note().get(), note.cpl_id().get());
2141  return compose("The CPL %1 has no <ContentVersion> tag", note.cpl_id().get());
2143  return compose("The namespace %1 in PKL %2 is invalid", note.note().get(), note.file()->filename());
2144  }
2145 
2146  return "";
2147 }
2148 
2149 
2150 bool
2151 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2152 {
2153  return a.type() == b.type() &&
2154  a.code() == b.code() &&
2155  a.note() == b.note() &&
2156  a.file() == b.file() &&
2157  a.line() == b.line() &&
2158  a.frame() == b.frame() &&
2159  a.component() == b.component() &&
2160  a.size() == b.size() &&
2161  a.id() == b.id() &&
2162  a.other_id() == b.other_id() &&
2163  a.frame_rate() == b.frame_rate() &&
2164  a.cpl_id() == b.cpl_id() &&
2165  a.reference_hash() == b.reference_hash() &&
2166  a.calculated_hash() == b.calculated_hash();
2167 }
2168 
2169 
2170 bool
2171 dcp::operator!=(dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2172 {
2173  return !(a == b);
2174 }
2175 
2176 
2177 bool
2178 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2179 {
2180  if (a.type() != b.type()) {
2181  return a.type() < b.type();
2182  }
2183 
2184  if (a.code() != b.code()) {
2185  return a.code() < b.code();
2186  }
2187 
2188  if (a.note() != b.note()) {
2189  return a.note().get_value_or("") < b.note().get_value_or("");
2190  }
2191 
2192  if (a.file() != b.file()) {
2193  return a.file().get_value_or("") < b.file().get_value_or("");
2194  }
2195 
2196  if (a.line() != b.line()) {
2197  return a.line().get_value_or(0) < b.line().get_value_or(0);
2198  }
2199 
2200  if (a.frame() != b.frame()) {
2201  return a.frame().get_value_or(0) < b.frame().get_value_or(0);
2202  }
2203 
2204  if (a.component() != b.component()) {
2205  return a.component().get_value_or(0) < b.component().get_value_or(0);
2206  }
2207 
2208  if (a.size() != b.size()) {
2209  return a.size().get_value_or(0) < b.size().get_value_or(0);
2210  }
2211 
2212  if (a.id() != b.id()) {
2213  return a.id().get_value_or("") < b.id().get_value_or("");
2214  }
2215 
2216  if (a.other_id() != b.other_id()) {
2217  return a.other_id().get_value_or("") < b.other_id().get_value_or("");
2218  }
2219 
2220  return a.frame_rate().get_value_or(0) != b.frame_rate().get_value_or(0);
2221 }
2222 
2223 
2224 std::ostream&
2225 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2226 {
2227  s << note_to_string (note);
2228  if (note.note()) {
2229  s << " [" << note.note().get() << "]";
2230  }
2231  if (note.file()) {
2232  s << " [" << note.file().get() << "]";
2233  }
2234  if (note.line()) {
2235  s << " [" << note.line().get() << "]";
2236  }
2237  return s;
2238 }
2239 
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:168
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:181
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:354
int64_t as_editable_units_floor(int tcr_) const
Definition: dcp_time.cc:347
std::string as_string(Standard standard) const
Definition: dcp_time.cc:332
@ BV21_ERROR
may not always be considered an error, but violates a "shall" requirement of Bv2.1
An XML error.
Definition: exceptions.h:191
CPL class.
DCP class.
Exceptions thrown by libdcp.
InteropTextAsset class.
MonoJ2KPictureAsset class.
MonoJ2KPictureFrame class.
Namespace for everything in libdcp.
Definition: array_data.h:50
void verify_j2k(std::shared_ptr< const Data > data, int start_index, int frame_index, int frame_rate, std::vector< VerificationNote > &notes)
Definition: verify_j2k.cc:68
@ FFEC
first frame of end credits
@ FFMC
first frame of moving credits
@ LFOC
last frame of composition
@ FFOC
first frame of composition
void verify_text_lines_and_characters(std::shared_ptr< const dcp::TextAsset > asset, int warning_length, int error_length, dcp::LinesCharactersResult *result)
Definition: verify.cc:1131
std::string make_digest(boost::filesystem::path filename, boost::function< void(int64_t, int64_t)>)
Methods for conversion to/from string.
Reel class.
ReelInteropTextAsset class.
ReelMarkersAsset class.
ReelPictureAsset class.
ReelSMPTETextAsset class.
ReelSoundAsset class.
ReelTextAsset class.
SMPTETextAsset class.
StereoJ2KPictureAsset class.
StereoJ2KPictureFrame class.
The integer, two-dimensional size of something.
Definition: types.h:71
boost::optional< boost::uintmax_t > maximum_asset_size_for_hash_check
< If set, any assets larger than this number of bytes will not have their hashes checked
Definition: verify.h:700
void verify_smpte_timed_text_asset(Context &context, shared_ptr< const SMPTETextAsset > asset, optional< int64_t > reel_asset_duration)
Definition: verify.cc:684
static void verify_closed_caption_asset(Context &context, shared_ptr< const TextAsset > asset, optional< int64_t > reel_asset_duration)
Definition: verify.cc:829
static void verify_text_details(Context &context, vector< shared_ptr< Reel >> reels, int edit_rate, std::function< bool(shared_ptr< Reel >)> check, std::function< optional< string >(shared_ptr< Reel >)> xml, std::function< int64_t(shared_ptr< Reel >)> duration, std::function< std::string(shared_ptr< Reel >)> id)
Definition: verify.cc:864
static void verify_subtitle_asset(Context &context, shared_ptr< const TextAsset > asset, optional< int64_t > reel_asset_duration)
Definition: verify.cc:784
void verify_smpte_subtitle_asset(Context &context, shared_ptr< const SMPTETextAsset > asset)
Definition: verify.cc:745
void verify_interop_text_asset(Context &context, shared_ptr< const InteropTextAsset > asset)
Definition: verify.cc:731
dcp::verify() method and associated code
Verification-related things exposed for testing.
Verification that JPEG2000 files meet requirements.