Support CPL metadata.
authorCarl Hetherington <cth@carlh.net>
Thu, 27 Aug 2020 20:59:12 +0000 (22:59 +0200)
committerCarl Hetherington <cth@carlh.net>
Sun, 20 Sep 2020 17:32:42 +0000 (19:32 +0200)
20 files changed:
src/cpl.cc
src/cpl.h
src/exceptions.cc
src/exceptions.h
src/language_tag.cc
src/language_tag.h
src/reel.cc
src/reel.h
src/reel_subtitle_asset.cc
src/reel_subtitle_asset.h
src/types.cc
src/types.h
test/cpl_metadata_test.cc [new file with mode: 0644]
test/reel_asset_test.cc
test/ref/cpl_metadata_test1.xml [new file with mode: 0644]
test/ref/cpl_metadata_test2.xml [new file with mode: 0644]
test/test.cc
test/test.h
test/verify_test.cc
test/wscript

index 6c5610d8e5a45f6bd7cdd2f09e7f39e757e721b3..8285f87e6fa18f829bb4614e7c0472997e8583fb 100644 (file)
 #include "local_time.h"
 #include "dcp_assert.h"
 #include "compose.hpp"
+#include "raw_convert.h"
 #include <libxml/parser.h>
 #include <libxml++/libxml++.h>
+#include <boost/algorithm/string.hpp>
 #include <boost/foreach.hpp>
 
 using std::string;
@@ -54,14 +56,18 @@ using std::list;
 using std::pair;
 using std::make_pair;
 using std::cout;
+using std::set;
 using std::vector;
 using boost::shared_ptr;
 using boost::optional;
 using boost::dynamic_pointer_cast;
 using namespace dcp;
 
+
 static string const cpl_interop_ns = "http://www.digicine.com/PROTO-ASDCP-CPL-20040511#";
 static string const cpl_smpte_ns   = "http://www.smpte-ra.org/schemas/429-7/2006/CPL";
+static string const cpl_metadata_ns = "http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata";
+
 
 CPL::CPL (string annotation_text, ContentKind content_kind)
        /* default _content_title_text to annotation_text */
@@ -122,6 +128,19 @@ CPL::CPL (boost::filesystem::path file)
        }
        _reels = type_grand_children<Reel> (f, "ReelList", "Reel");
 
+       cxml::ConstNodePtr reel_list = f.node_child ("ReelList");
+       if (reel_list) {
+               list<cxml::NodePtr> reels = reel_list->node_children("Reel");
+               if (!reels.empty()) {
+                       cxml::ConstNodePtr asset_list = reels.front()->node_child("AssetList");
+                       cxml::ConstNodePtr metadata = asset_list->optional_node_child("CompositionMetadataAsset");
+                       if (metadata) {
+                               read_composition_metadata_asset (metadata);
+                       }
+               }
+       }
+
+
        f.ignore_child ("Issuer");
        f.ignore_child ("Signer");
        f.ignore_child ("Signature");
@@ -139,6 +158,7 @@ CPL::add (boost::shared_ptr<Reel> reel)
 }
 
 /** Write an CompositonPlaylist XML file.
+ *
  *  @param file Filename to write.
  *  @param standard INTEROP or SMPTE.
  *  @param signer Signer to sign the CPL, or 0 to add no signature.
@@ -171,8 +191,13 @@ CPL::write_xml (boost::filesystem::path file, Standard standard, shared_ptr<cons
 
        xmlpp::Element* reel_list = root->add_child ("ReelList");
 
+       bool first = true;
        BOOST_FOREACH (shared_ptr<Reel> i, _reels) {
-               i->write_to_cpl (reel_list, standard);
+               xmlpp::Element* asset_list = i->write_to_cpl (reel_list, standard);
+               if (first && standard == dcp::SMPTE) {
+                       maybe_write_composition_metadata_asset (asset_list);
+                       first = false;
+               }
        }
 
        indent (root, 0);
@@ -186,6 +211,191 @@ CPL::write_xml (boost::filesystem::path file, Standard standard, shared_ptr<cons
        set_file (file);
 }
 
+
+void
+CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
+{
+       cxml::ConstNodePtr fctt = node->node_child("FullContentTitleText");
+       _full_content_title_text = fctt->content();
+       _full_content_title_text_language = fctt->optional_string_attribute("language");
+
+       _release_territory = node->optional_string_child("ReleaseTerritory");
+
+       cxml::ConstNodePtr vn = node->optional_node_child("VersionNumber");
+       if (vn) {
+               _version_number = raw_convert<int>(vn->content());
+               /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
+               optional<string> vn_status = vn->optional_string_attribute("status");
+               if (vn_status) {
+                       _status = string_to_status (*vn_status);
+               }
+       }
+
+       _chain = node->optional_string_child("Chain");
+       _distributor = node->optional_string_child("Distributor");
+       _facility = node->optional_string_child("Facility");
+
+       cxml::ConstNodePtr acv = node->optional_node_child("AlternateContentVersionList");
+       if (acv) {
+               BOOST_FOREACH (cxml::ConstNodePtr i, acv->node_children("ContentVersion")) {
+                       _content_versions.push_back (ContentVersion(i));
+               }
+       }
+
+       cxml::ConstNodePtr lum = node->optional_node_child("Luminance");
+       if (lum) {
+               _luminance = Luminance (lum);
+       }
+
+       _main_sound_configuration = node->string_child("MainSoundConfiguration");
+
+       string sr = node->string_child("MainSoundSampleRate");
+       vector<string> sr_bits;
+       boost::split (sr_bits, sr, boost::is_any_of(" "));
+       DCP_ASSERT (sr_bits.size() == 2);
+       _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
+
+       _main_picture_stored_area = dcp::Size (
+               node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
+               node->node_child("MainPictureStoredArea")->number_child<int>("Height")
+               );
+
+       _main_picture_active_area = dcp::Size (
+               node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
+               node->node_child("MainPictureActiveArea")->number_child<int>("Height")
+               );
+
+       optional<string> sll = node->optional_string_child("MainSubtitleLanguageList");
+       if (sll) {
+               vector<string> sll_split;
+               boost::split (sll_split, *sll, boost::is_any_of(" "));
+               DCP_ASSERT (!sll_split.empty());
+
+               /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
+               size_t first = 0;
+               if (!_reels.empty()) {
+                       shared_ptr<dcp::ReelSubtitleAsset> sub = _reels.front()->main_subtitle();
+                       if (sub) {
+                               optional<dcp::LanguageTag> lang = sub->language();
+                               if (lang && lang->to_string() == sll_split[0]) {
+                                       first = 1;
+                               }
+                       }
+               }
+
+               for (size_t i = first; i < sll_split.size(); ++i) {
+                       _additional_subtitle_languages.push_back (sll_split[i]);
+               }
+       }
+}
+
+
+/** Write a CompositionMetadataAsset node as a child of @param node provided
+ *  the required metadata is stored in the object.  If any required metadata
+ *  is missing this method will do nothing.
+ */
+void
+CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
+{
+       if (
+               !_main_sound_configuration ||
+               !_main_sound_sample_rate ||
+               !_main_picture_stored_area ||
+               !_main_picture_active_area ||
+               _reels.empty() ||
+               !_reels.front()->main_picture()) {
+               return;
+       }
+
+       xmlpp::Element* meta = node->add_child("meta:CompositionMetadataAsset");
+       meta->set_namespace_declaration (cpl_metadata_ns, "meta");
+
+       meta->add_child("Id")->add_child_text("urn:uuid:" + make_uuid());
+
+       shared_ptr<dcp::ReelPictureAsset> mp = _reels.front()->main_picture();
+       meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
+       meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
+
+       xmlpp::Element* fctt = meta->add_child("FullContentTitleText", "meta");
+       if (_full_content_title_text) {
+               fctt->add_child_text (*_full_content_title_text);
+       }
+       if (_full_content_title_text_language) {
+               fctt->set_attribute("language", *_full_content_title_text_language);
+       }
+
+       if (_release_territory) {
+               meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
+       }
+
+       if (_version_number) {
+               xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
+               vn->add_child_text(raw_convert<string>(*_version_number));
+               if (_status) {
+                       vn->set_attribute("status", status_to_string(*_status));
+               }
+       }
+
+       if (_chain) {
+               meta->add_child("Chain", "meta")->add_child_text(*_chain);
+       }
+
+       if (_distributor) {
+               meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
+       }
+
+       if (_facility) {
+               meta->add_child("Facility", "meta")->add_child_text(*_facility);
+       }
+
+       if (_content_versions.size() > 1) {
+               xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
+               for (size_t i = 1; i < _content_versions.size(); ++i) {
+                       _content_versions[i].as_xml (vc);
+               }
+       }
+
+       if (_luminance) {
+               _luminance->as_xml (meta, "meta");
+       }
+
+       meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
+       meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
+
+       xmlpp::Element* stored = meta->add_child("MainPictureStoredArea", "meta");
+       stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
+       stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
+
+       xmlpp::Element* active = meta->add_child("MainPictureActiveArea", "meta");
+       active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
+       active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
+
+       optional<dcp::LanguageTag> first_subtitle_language;
+       BOOST_FOREACH (shared_ptr<const Reel> i, _reels) {
+               if (i->main_subtitle()) {
+                       first_subtitle_language = i->main_subtitle()->language();
+                       if (first_subtitle_language) {
+                               break;
+                       }
+               }
+       }
+
+       if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
+               string lang;
+               if (first_subtitle_language) {
+                       lang = first_subtitle_language->to_string();
+               }
+               BOOST_FOREACH (dcp::LanguageTag const& i, _additional_subtitle_languages) {
+                       if (!lang.empty()) {
+                               lang += " ";
+                       }
+                       lang += i.to_string();
+               }
+               meta->add_child("MainSubtitleLanguageList")->add_child_text(lang);
+       }
+}
+
+
 list<shared_ptr<ReelMXF> >
 CPL::reel_mxfs ()
 {
@@ -337,6 +547,19 @@ CPL::duration () const
        }
        return d;
 }
+
+
+void
+CPL::set_version_number (int v)
+{
+       if (v < 0) {
+               throw BadSettingError ("CPL version number cannot be negative");
+       }
+
+       _version_number = v;
+}
+
+
 void
 CPL::set_content_versions (vector<ContentVersion> v)
 {
@@ -357,3 +580,13 @@ CPL::content_version () const
        DCP_ASSERT (!_content_versions.empty());
        return _content_versions[0];
 }
+
+
+void
+CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
+{
+       _additional_subtitle_languages.clear ();
+       BOOST_FOREACH (dcp::LanguageTag const& i, langs) {
+               _additional_subtitle_languages.push_back (i.to_string());
+       }
+}
index bb8975c91d2d2d0d2402a2bdb13950671389cf93..72366ee3d9bbe0a2f6e996c4614a524fa212036b 100644 (file)
--- a/src/cpl.h
+++ b/src/cpl.h
@@ -44,6 +44,7 @@
 #include "asset.h"
 #include "certificate.h"
 #include "key.h"
+#include "language_tag.h"
 #include "types.h"
 #include <boost/filesystem.hpp>
 #include <boost/function.hpp>
@@ -160,6 +161,114 @@ public:
                _ratings = r;
        }
 
+       boost::optional<std::string> full_content_title_text () const {
+               return _full_content_title_text;
+       }
+
+       void set_full_content_title_text (std::string t) {
+               _full_content_title_text = t;
+       }
+
+       boost::optional<std::string> full_content_title_text_language () const {
+               return _full_content_title_text_language;
+       }
+
+       void set_full_content_title_text_language (dcp::LanguageTag l) {
+               _full_content_title_text_language = l.to_string();
+       }
+
+       boost::optional<std::string> release_territory () const {
+               return _release_territory;
+       }
+
+       void set_release_territory (dcp::LanguageTag::RegionSubtag t) {
+               _release_territory = t.subtag();
+       }
+
+       boost::optional<int> version_number () const {
+               return _version_number;
+       }
+
+       void set_version_number (int v);
+
+       boost::optional<Status> status () const {
+               return _status;
+       }
+
+       void set_status (Status s) {
+               _status = s;
+       }
+
+       boost::optional<std::string> chain () const {
+               return _chain;
+       }
+
+       void set_chain (std::string c) {
+               _chain = c;
+       }
+
+       boost::optional<std::string> distributor () const {
+               return _distributor;
+       }
+
+       void set_distributor (std::string d) {
+               _distributor = d;
+       }
+
+       boost::optional<std::string> facility () const {
+               return _facility;
+       }
+
+       void set_facility (std::string f) {
+               _facility = f;
+       }
+
+       boost::optional<Luminance> luminance () const {
+               return _luminance;
+       }
+
+       void set_luminance (Luminance l) {
+               _luminance = l;
+       }
+
+       boost::optional<std::string> main_sound_configuration () const {
+               return _main_sound_configuration;
+       }
+
+       void set_main_sound_configuration (std::string c) {
+               _main_sound_configuration = c;
+       }
+
+       boost::optional<int> main_sound_sample_rate () const {
+               return _main_sound_sample_rate;
+       }
+
+       void set_main_sound_sample_rate (int r) {
+               _main_sound_sample_rate = r;
+       }
+
+       boost::optional<dcp::Size> main_picture_stored_area () const {
+               return _main_picture_stored_area;
+       }
+
+       void set_main_picture_stored_area (dcp::Size s) {
+               _main_picture_stored_area = s;
+       }
+
+       boost::optional<dcp::Size> main_picture_active_area () const {
+               return _main_picture_active_area;
+       }
+
+       void set_main_picture_active_area (dcp::Size s) {
+               _main_picture_active_area = s;
+       }
+
+       std::vector<std::string> additional_subtitle_languages () const {
+               return _additional_subtitle_languages;
+       }
+
+       void set_additional_subtitle_languages (std::vector<dcp::LanguageTag> const& lang);
+
        boost::optional<Standard> standard () const {
                return _standard;
        }
@@ -171,6 +280,9 @@ protected:
        std::string pkl_type (Standard standard) const;
 
 private:
+       void maybe_write_composition_metadata_asset (xmlpp::Element* node) const;
+       void read_composition_metadata_asset (cxml::ConstNodePtr node);
+
        std::string _issuer;
        std::string _creator;
        std::string _issue_date;
@@ -179,6 +291,25 @@ private:
        ContentKind _content_kind;                  ///< &lt;ContentKind&gt;
        std::vector<ContentVersion> _content_versions;
        std::vector<Rating> _ratings;
+       /** Human-readable name of the composition, without any metadata (i.e. no -FTR-EN-XX- etc.) */
+       boost::optional<std::string> _full_content_title_text;
+       boost::optional<std::string> _full_content_title_text_language;
+       /** This is stored and returned as a string so that we can tolerate non-RFC-5646 strings,
+        *  but must be set as a dcp::LanguageTag to try to ensure that we create compliant output.
+        */
+       boost::optional<std::string> _release_territory;
+       boost::optional<int> _version_number;
+       boost::optional<Status> _status;
+       boost::optional<std::string> _chain;
+       boost::optional<std::string> _distributor;
+       boost::optional<std::string> _facility;
+       boost::optional<Luminance> _luminance;
+       boost::optional<std::string> _main_sound_configuration;
+       boost::optional<int> _main_sound_sample_rate;
+       boost::optional<dcp::Size> _main_picture_stored_area;
+       boost::optional<dcp::Size> _main_picture_active_area;
+       /* See note for _release_territory above */
+       std::vector<std::string> _additional_subtitle_languages;
 
        std::list<boost::shared_ptr<Reel> > _reels;
 
index 70320b528fd0ac290c846f4f0dd9655d58e9a8fc..16676e20f70ac6560096a4c74ed1aeb489158026 100644 (file)
@@ -137,6 +137,30 @@ CombineError::CombineError (string message)
        : runtime_error (message)
 {}
 
+
 LanguageTagError::LanguageTagError (std::string message)
        : runtime_error (message)
 {}
+
+
+BadSettingError::BadSettingError (std::string message)
+       : runtime_error (message)
+{
+
+}
+
+
+DuplicateIdError::DuplicateIdError (std::string message)
+       : runtime_error (message)
+{
+
+}
+
+
+MainSoundConfigurationError::MainSoundConfigurationError (std::string s)
+       : runtime_error (String::compose("Could not parse MainSoundConfiguration %1", s))
+{
+
+}
+
+>>>>>>> Support CPL metadata.
index 79ee9fca285bb2a4002a7c17a30a421bdcd1345e..12624175cafe4a237c9f2dc3ecfd0ee0a894e433 100644 (file)
@@ -254,6 +254,28 @@ public:
        LanguageTagError (std::string message);
 };
 
+
+class BadSettingError : public std::runtime_error
+{
+public:
+       BadSettingError (std::string message);
+};
+
+
+class DuplicateIdError : public std::runtime_error
+{
+public:
+       DuplicateIdError (std::string message);
+};
+
+
+class MainSoundConfigurationError : public std::runtime_error
+{
+public:
+       MainSoundConfigurationError (std::string s);
+};
+
+
 }
 
 #endif
index b64dab72ea763036587edcbb4676a31f4de32a93..4ac90507101b3c20ae0caa356c6e83a4261b040d 100644 (file)
@@ -41,6 +41,9 @@
 #include <string>
 
 
+using std::make_pair;
+using std::ostream;
+using std::pair;
 using std::string;
 using std::vector;
 using boost::optional;
@@ -64,31 +67,10 @@ find_in_list (LanguageTag::SubtagData* list, int length, string subtag)
 }
 
 
-static
-optional<LanguageTag::SubtagData>
-find_in_list (LanguageTag::SubtagType type, string subtag)
-{
-       switch (type) {
-       case dcp::LanguageTag::LANGUAGE:
-               return find_in_list(language_list, sizeof(language_list) / sizeof(LanguageTag::SubtagData), subtag);
-       case dcp::LanguageTag::SCRIPT:
-               return find_in_list(script_list, sizeof(script_list) / sizeof(LanguageTag::SubtagData), subtag);
-       case dcp::LanguageTag::REGION:
-               return find_in_list(region_list, sizeof(region_list) / sizeof(LanguageTag::SubtagData), subtag);
-       case dcp::LanguageTag::VARIANT:
-               return find_in_list(variant_list, sizeof(variant_list) / sizeof(LanguageTag::SubtagData), subtag);
-       case dcp::LanguageTag::EXTLANG:
-               return find_in_list(extlang_list, sizeof(extlang_list) / sizeof(LanguageTag::SubtagData), subtag);
-       }
-
-       return optional<LanguageTag::SubtagData>();
-}
-
-
 LanguageTag::Subtag::Subtag (string subtag, SubtagType type)
        : _subtag (subtag)
 {
-       if (!find_in_list(type, subtag)) {
+       if (!get_subtag_data(type, subtag)) {
                throw LanguageTagError(String::compose("Unknown %1 string %2", subtag_type_name(type), subtag));
        }
 }
@@ -268,29 +250,29 @@ LanguageTag::description () const
        string d;
 
        BOOST_FOREACH (VariantSubtag const& i, _variants) {
-               optional<SubtagData> variant = find_in_list (VARIANT, i.subtag());
+               optional<SubtagData> variant = get_subtag_data (VARIANT, i.subtag());
                DCP_ASSERT (variant);
                d += variant->description + " dialect of ";
        }
 
-       optional<SubtagData> language = find_in_list (LANGUAGE, _language->subtag());
+       optional<SubtagData> language = get_subtag_data (LANGUAGE, _language->subtag());
        DCP_ASSERT (language);
        d += language->description;
 
        if (_script) {
-               optional<SubtagData> script = find_in_list (SCRIPT, _script->subtag());
+               optional<SubtagData> script = get_subtag_data (SCRIPT, _script->subtag());
                DCP_ASSERT (script);
                d += " written using the " + script->description + " script";
        }
 
        if (_region) {
-               optional<SubtagData> region = find_in_list (REGION, _region->subtag());
+               optional<SubtagData> region = get_subtag_data (REGION, _region->subtag());
                DCP_ASSERT (region);
                d += " for " + region->description;
        }
 
        BOOST_FOREACH (ExtlangSubtag const& i, _extlangs) {
-               optional<SubtagData> extlang = find_in_list (EXTLANG, i.subtag());
+               optional<SubtagData> extlang = get_subtag_data (EXTLANG, i.subtag());
                DCP_ASSERT (extlang);
                d += ", " + extlang->description;
        }
@@ -381,3 +363,80 @@ dcp::LanguageTag::ExtlangSubtag::operator< (ExtlangSubtag const & other) const
 {
        return subtag() < other.subtag();
 }
+
+
+bool
+dcp::operator== (dcp::LanguageTag const& a, dcp::LanguageTag const& b)
+{
+       return a.to_string() == b.to_string();
+}
+
+
+ostream&
+dcp::operator<< (ostream& os, dcp::LanguageTag const& tag)
+{
+       os << tag.to_string();
+       return os;
+}
+
+
+vector<pair<LanguageTag::SubtagType, LanguageTag::SubtagData> >
+LanguageTag::subtags () const
+{
+       vector<pair<SubtagType, SubtagData> > s;
+
+       if (_language) {
+               s.push_back (make_pair(LANGUAGE, *get_subtag_data(LANGUAGE, _language->subtag())));
+       }
+
+       if (_script) {
+               s.push_back (make_pair(SCRIPT, *get_subtag_data(SCRIPT, _script->subtag())));
+       }
+
+       if (_region) {
+               s.push_back (make_pair(REGION, *get_subtag_data(REGION, _region->subtag())));
+       }
+
+       BOOST_FOREACH (VariantSubtag const& i, _variants) {
+               s.push_back (make_pair(VARIANT, *get_subtag_data(VARIANT, i.subtag())));
+       }
+
+       BOOST_FOREACH (ExtlangSubtag const& i, _extlangs) {
+               s.push_back (make_pair(EXTLANG, *get_subtag_data(EXTLANG, i.subtag())));
+       }
+
+       return s;
+}
+
+
+optional<LanguageTag::SubtagData>
+LanguageTag::get_subtag_data (LanguageTag::SubtagType type, string subtag)
+{
+       switch (type) {
+       case dcp::LanguageTag::LANGUAGE:
+               return find_in_list(language_list, sizeof(language_list) / sizeof(LanguageTag::SubtagData), subtag);
+       case dcp::LanguageTag::SCRIPT:
+               return find_in_list(script_list, sizeof(script_list) / sizeof(LanguageTag::SubtagData), subtag);
+       case dcp::LanguageTag::REGION:
+               return find_in_list(region_list, sizeof(region_list) / sizeof(LanguageTag::SubtagData), subtag);
+       case dcp::LanguageTag::VARIANT:
+               return find_in_list(variant_list, sizeof(variant_list) / sizeof(LanguageTag::SubtagData), subtag);
+       case dcp::LanguageTag::EXTLANG:
+               return find_in_list(extlang_list, sizeof(extlang_list) / sizeof(LanguageTag::SubtagData), subtag);
+       }
+
+       return optional<LanguageTag::SubtagData>();
+}
+
+
+optional<string>
+LanguageTag::get_subtag_description (LanguageTag::SubtagType type, string subtag)
+{
+       optional<SubtagData> data = get_subtag_data (type, subtag);
+       if (!data) {
+               return optional<string>();
+       }
+
+       return data->description;
+}
+
index 8aa8a723db3ee8ea4923029c863a7463b3185d6c..74b4a8a77023a774ed2cc31234388d8dd0715bb1 100644 (file)
@@ -82,6 +82,12 @@ public:
                        return _subtag;
                }
 
+               virtual SubtagType type () const = 0;
+
+               bool operator== (Subtag const& other) {
+                       return _subtag == other._subtag;
+               }
+
        protected:
                Subtag (std::string subtag, SubtagType type);
 
@@ -96,6 +102,10 @@ public:
                        : Subtag(subtag, LANGUAGE) {}
                LanguageSubtag (char const* subtag)
                        : Subtag(subtag, LANGUAGE) {}
+
+               SubtagType type () const {
+                       return LANGUAGE;
+               }
        };
 
        class ScriptSubtag : public Subtag
@@ -105,6 +115,10 @@ public:
                        : Subtag(subtag, SCRIPT) {}
                ScriptSubtag (char const* subtag)
                        : Subtag(subtag, SCRIPT) {}
+
+               SubtagType type () const {
+                       return SCRIPT;
+               }
        };
 
        class RegionSubtag : public Subtag
@@ -114,6 +128,10 @@ public:
                        : Subtag(subtag, REGION) {}
                RegionSubtag (char const* subtag)
                        : Subtag(subtag, REGION) {}
+
+               SubtagType type () const {
+                       return REGION;
+               }
        };
 
        class VariantSubtag : public Subtag
@@ -124,6 +142,10 @@ public:
                VariantSubtag (char const* subtag)
                        : Subtag(subtag, VARIANT) {}
 
+               SubtagType type () const {
+                       return VARIANT;
+               }
+
                bool operator== (VariantSubtag const& other) const;
                bool operator< (VariantSubtag const& other) const;
        };
@@ -137,6 +159,10 @@ public:
                ExtlangSubtag (char const* subtag)
                        : Subtag(subtag, EXTLANG) {}
 
+               SubtagType type () const {
+                       return EXTLANG;
+               }
+
                bool operator== (ExtlangSubtag const& other) const;
                bool operator< (ExtlangSubtag const& other) const;
        };
@@ -144,18 +170,58 @@ public:
        LanguageTag () {}
        LanguageTag (std::string tag);
 
+       boost::optional<LanguageSubtag> language() {
+               return _language;
+       }
+
        void set_language (LanguageSubtag language);
+
+       boost::optional<ScriptSubtag> script() const {
+               return _script;
+       }
+
        void set_script (ScriptSubtag script);
+
+       boost::optional<RegionSubtag> region() const {
+               return _region;
+       }
+
        void set_region (RegionSubtag region);
+
+       std::vector<VariantSubtag> variants() const {
+               return _variants;
+       }
+
        void set_variants (std::vector<VariantSubtag> variants);
        void add_variant (VariantSubtag variant);
+
+       std::vector<ExtlangSubtag> extlangs() const {
+               return _extlangs;
+       }
+
        void set_extlangs (std::vector<ExtlangSubtag> extlangs);
        void add_extlang (ExtlangSubtag extlang);
 
+       std::vector<std::pair<SubtagType, SubtagData> > subtags () const;
+
        static std::vector<SubtagData> get_all (SubtagType type);
        static std::string subtag_type_name (SubtagType type);
 
+       static boost::optional<std::string> get_subtag_description (SubtagType, std::string subtag);
+       static boost::optional<SubtagData > get_subtag_data (SubtagType, std::string subtag);
+
+       template <class T>
+       static boost::optional<std::string> get_subtag_description (T s) {
+               return get_subtag_description (s.type(), s.subtag());
+       }
+
+       template <class T>
+       static boost::optional<SubtagData> get_subtag_data (T s) {
+               return get_subtag_data (s.type(), s.subtag());
+       }
+
 private:
+
        boost::optional<LanguageSubtag> _language;
        boost::optional<ScriptSubtag> _script;
        boost::optional<RegionSubtag> _region;
@@ -163,6 +229,8 @@ private:
        std::vector<ExtlangSubtag> _extlangs;
 };
 
+extern bool operator==(dcp::LanguageTag const& a, dcp::LanguageTag const& b);
+extern std::ostream& operator<<(std::ostream& os, dcp::LanguageTag const& tag);
 
 }
 
index 453a3163184d182ad0403153de9c96f225df5f88..d3e2a8503b47f0a9926d79f0ea42a9be98d82baf 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2014-2017 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2014-2020 Carl Hetherington <cth@carlh.net>
 
     This file is part of libdcp.
 
@@ -115,7 +115,7 @@ Reel::Reel (boost::shared_ptr<const cxml::Node> node)
        node->done ();
 }
 
-void
+xmlpp::Element *
 Reel::write_to_cpl (xmlpp::Element* node, Standard standard) const
 {
        xmlpp::Element* reel = node->add_child ("Reel");
@@ -151,6 +151,8 @@ Reel::write_to_cpl (xmlpp::Element* node, Standard standard) const
        if (_atmos) {
                _atmos->write_to_cpl (asset_list, standard);
        }
+
+       return asset_list;
 }
 
 bool
index 3792682e52b9440847de4390c746369a350f189c..ccad3c83229f5ee49c04d774b778d0b75e270ad2 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2020 Carl Hetherington <cth@carlh.net>
 
     This file is part of libdcp.
 
@@ -113,7 +113,7 @@ public:
 
        std::list<boost::shared_ptr<ReelAsset> > assets () const;
 
-       void write_to_cpl (xmlpp::Element* node, Standard standard) const;
+       xmlpp::Element* write_to_cpl (xmlpp::Element* node, Standard standard) const;
 
        bool encrypted () const;
 
index 4fa9fd0a26aa3cee4ed76911784b522503ceafaf..f7df36f85d8d82976c2c93e9c1f9de33acc48226 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2012-2017 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2020 Carl Hetherington <cth@carlh.net>
 
     This file is part of libdcp.
 
@@ -35,6 +35,7 @@
  *  @brief ReelSubtitleAsset class.
  */
 
+#include "language_tag.h"
 #include "subtitle_asset.h"
 #include "reel_subtitle_asset.h"
 #include "smpte_subtitle_asset.h"
@@ -57,7 +58,10 @@ ReelSubtitleAsset::ReelSubtitleAsset (boost::shared_ptr<const cxml::Node> node)
        : ReelAsset (node)
        , ReelMXF (node)
 {
-       node->ignore_child ("Language");
+       optional<string> const language = node->optional_string_child("Language");
+       if (language) {
+               _language = dcp::LanguageTag(*language);
+       }
        node->done ();
 }
 
@@ -78,6 +82,9 @@ ReelSubtitleAsset::write_to_cpl (xmlpp::Node* node, Standard standard) const
 {
        xmlpp::Node* asset = write_to_cpl_asset (node, standard, hash());
        write_to_cpl_mxf (asset);
+       if (_language) {
+               asset->add_child("Language")->add_child_text(_language->to_string());
+       }
        return asset;
 }
 
index 4cc03cf1a7d2ffcc0fee741a267d94a0b3cdddbf..123478cfd737d3700d07d69ec0702b891f64099d 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2012-2017 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2020 Carl Hetherington <cth@carlh.net>
 
     This file is part of libdcp.
 
@@ -38,6 +38,7 @@
 #ifndef LIBDCP_REEL_SUBTITLE_ASSET_H
 #define LIBDCP_REEL_SUBTITLE_ASSET_H
 
+#include "language_tag.h"
 #include "reel_asset.h"
 #include "reel_mxf.h"
 #include "subtitle_asset.h"
@@ -62,9 +63,17 @@ public:
                return asset_of_type<SubtitleAsset> ();
        }
 
+       void set_language (dcp::LanguageTag language);
+
+       boost::optional<dcp::LanguageTag> language () const {
+               return _language;
+       }
+
 private:
        std::string key_type () const;
        std::string cpl_node_name (Standard standard) const;
+
+       boost::optional<dcp::LanguageTag> _language;
 };
 
 }
index 746b6f6a81e234385225cbec68891448dc478132..9d67ee67bfb4292bf5281433f15699fce04337bb 100644 (file)
 #include "dcp_assert.h"
 #include <libxml++/libxml++.h>
 #include <boost/algorithm/string.hpp>
+#include <boost/foreach.hpp>
+#include <string>
 #include <vector>
+#include <cmath>
 #include <cstdio>
 #include <iomanip>
 
-using namespace std;
+using std::string;
+using std::ostream;
+using std::vector;
 using namespace dcp;
 using namespace boost;
 
@@ -464,6 +469,28 @@ dcp::operator<< (ostream& s, Rating const & r)
 }
 
 
+ContentVersion::ContentVersion ()
+       : id ("urn:uuid:" + make_uuid())
+{
+
+}
+
+
+ContentVersion::ContentVersion (cxml::ConstNodePtr node)
+{
+       id = node->string_child("Id");
+       label_text = node->string_child("LabelText");
+}
+
+
+ContentVersion::ContentVersion (string label_text_)
+       : id ("urn:uuid:" + make_uuid())
+       , label_text (label_text_)
+{
+
+}
+
+
 void
 ContentVersion::as_xml (xmlpp::Element* parent) const
 {
@@ -472,3 +499,261 @@ ContentVersion::as_xml (xmlpp::Element* parent) const
        cv->add_child("LabelText")->add_child_text(label_text);
 }
 
+
+Luminance::Luminance (cxml::ConstNodePtr node)
+{
+       _unit = string_to_unit (node->string_attribute("units"));
+       _value = raw_convert<float> (node->content());
+}
+
+
+Luminance::Luminance (float value, Unit unit)
+       : _unit (unit)
+{
+       set_value (value);
+}
+
+
+void
+Luminance::set_value (float v)
+{
+       if (v < 0) {
+               throw dcp::MiscError (String::compose("Invalid luminance value %1", v));
+       }
+
+       _value = v;
+}
+
+
+void
+Luminance::as_xml (xmlpp::Element* parent, string ns) const
+{
+       xmlpp::Element* lum = parent->add_child("Luminance", ns);
+       lum->set_attribute("units", unit_to_string(_unit));
+       lum->add_child_text(raw_convert<string>(_value, 3));
+}
+
+
+string
+Luminance::unit_to_string (Unit u)
+{
+       switch (u) {
+       case CANDELA_PER_SQUARE_METRE:
+               return "candela-per-square-metre";
+       case FOOT_LAMBERT:
+               return "foot-lambert";
+       default:
+               DCP_ASSERT (false);
+       }
+
+       return "";
+}
+
+
+Luminance::Unit
+Luminance::string_to_unit (string u)
+{
+       if (u == "candela-per-square-metre") {
+               return Unit::CANDELA_PER_SQUARE_METRE;
+       } else if (u == "foot-lambert") {
+               return Unit::FOOT_LAMBERT;
+       }
+
+       throw XMLError (String::compose("Invalid luminance unit %1", u));
+}
+
+
+bool
+dcp::operator== (Luminance const& a, Luminance const& b)
+{
+       return fabs(a.value() - b.value()) < 0.001 && a.unit() == b.unit();
+}
+
+
+MainSoundConfiguration::MainSoundConfiguration (string s)
+{
+       vector<string> parts;
+       boost::split (parts, s, boost::is_any_of("/"));
+       if (parts.size() != 2) {
+               throw MainSoundConfigurationError (s);
+       }
+
+       if (parts[0] == "51") {
+               _field = FIVE_POINT_ONE;
+       } else if (parts[0] == "71") {
+               _field = SEVEN_POINT_ONE;
+       } else {
+               throw MainSoundConfigurationError (s);
+       }
+
+       vector<string> channels;
+       boost::split (channels, parts[1], boost::is_any_of(","));
+
+       if (channels.size() > 16) {
+               throw MainSoundConfigurationError (s);
+       }
+
+       BOOST_FOREACH (string i, channels) {
+               if (i == "-") {
+                       _channels.push_back(optional<Channel>());
+               } else if (i == "L") {
+                       _channels.push_back(LEFT);
+               } else if (i == "R") {
+                       _channels.push_back(RIGHT);
+               } else if (i == "C") {
+                       _channels.push_back(CENTRE);
+               } else if (i == "LFE") {
+                       _channels.push_back(LFE);
+               } else if (i == "Ls" || i == "Lss") {
+                       _channels.push_back(LS);
+               } else if (i == "Rs" || i == "Rss") {
+                       _channels.push_back(RS);
+               } else if (i == "HI") {
+                       _channels.push_back(HI);
+               } else if (i == "VIN") {
+                       _channels.push_back(VI);
+               } else if (i == "Lrs") {
+                       _channels.push_back(BSL);
+               } else if (i == "Rrs") {
+                       _channels.push_back(BSR);
+               } else if (i == "DBOX") {
+                       _channels.push_back(MOTION_DATA);
+               } else if (i == "Sync") {
+                       _channels.push_back(SYNC_SIGNAL);
+               } else if (i == "Sign") {
+                       _channels.push_back(SIGN_LANGUAGE);
+               } else {
+                       throw MainSoundConfigurationError (s);
+               }
+       }
+}
+
+
+MainSoundConfiguration::MainSoundConfiguration (Field field, int channels)
+       : _field (field)
+{
+       _channels.resize (channels);
+}
+
+
+string
+MainSoundConfiguration::to_string () const
+{
+       string c;
+       if (_field == FIVE_POINT_ONE) {
+               c = "51/";
+       } else {
+               c = "71/";
+       }
+
+       BOOST_FOREACH (optional<Channel> i, _channels) {
+               if (!i) {
+                       c += "-,";
+               } else {
+                       switch (*i) {
+                       case LEFT:
+                               c += "L,";
+                               break;
+                       case RIGHT:
+                               c += "R,";
+                               break;
+                       case CENTRE:
+                               c += "C,";
+                               break;
+                       case LFE:
+                               c += "LFE,";
+                               break;
+                       case LS:
+                               c += (_field == FIVE_POINT_ONE ? "Ls," : "Lss,");
+                               break;
+                       case RS:
+                               c += (_field == FIVE_POINT_ONE ? "Rs," : "Rss,");
+                               break;
+                       case HI:
+                               c += "HI,";
+                               break;
+                       case VI:
+                               c += "VIN,";
+                               break;
+                       case LC:
+                       case RC:
+                               c += "-,";
+                               break;
+                       case BSL:
+                               c += "Lrs,";
+                               break;
+                       case BSR:
+                               c += "Rrs,";
+                               break;
+                       case MOTION_DATA:
+                               c += "DBOX,";
+                               break;
+                       case SYNC_SIGNAL:
+                               c += "Sync,";
+                               break;
+                       case SIGN_LANGUAGE:
+                               /* XXX: not sure what this should be */
+                               c += "Sign,";
+                               break;
+                       default:
+                               c += "-,";
+                               break;
+                       }
+               }
+       }
+
+       if (c.length() > 0) {
+               c = c.substr(0, c.length() - 1);
+       }
+
+       return c;
+}
+
+
+optional<Channel>
+MainSoundConfiguration::mapping (int index) const
+{
+       DCP_ASSERT (static_cast<size_t>(index) < _channels.size());
+       return _channels[index];
+}
+
+
+void
+MainSoundConfiguration::set_mapping (int index, Channel c)
+{
+       DCP_ASSERT (static_cast<size_t>(index) < _channels.size());
+       _channels[index] = c;
+}
+
+
+string
+dcp::status_to_string (Status s)
+{
+       switch (s) {
+       case FINAL:
+               return "final";
+       case TEMP:
+               return "temp";
+       case PRE:
+               return "pre";
+       default:
+               DCP_ASSERT (false);
+       }
+
+}
+
+
+Status
+dcp::string_to_status (string s)
+{
+       if (s == "final") {
+               return FINAL;
+       } else if (s == "temp") {
+               return TEMP;
+       } else if (s == "pre") {
+               return PRE;
+       }
+
+       DCP_ASSERT (false);
+}
+
index f8a9f45e1acfe02c308e467ce54f69fd19ef89b2..9a4b3e13a03bde432f228646ab07f8905c74109a 100644 (file)
@@ -328,10 +328,26 @@ extern bool operator== (Rating const & a, Rating const & b);
 extern std::ostream& operator<< (std::ostream& s, Rating const & r);
 
 
+enum Status
+{
+       FINAL, ///< final version
+       TEMP,  ///< temporary version (picture/sound unfinished)
+       PRE    ///< pre-release (picture/sound finished)
+};
+
+
+extern std::string status_to_string (Status s);
+extern Status string_to_status (std::string s);
+
+
 class ContentVersion
 {
 public:
-       ContentVersion () {}
+       ContentVersion ();
+
+       explicit ContentVersion (cxml::ConstNodePtr node);
+
+       explicit ContentVersion (std::string label_text_);
 
        ContentVersion (std::string id_, std::string label_text_)
                : id (id_)
@@ -344,6 +360,75 @@ public:
        std::string label_text;
 };
 
+
+class Luminance
+{
+public:
+       enum Unit {
+               CANDELA_PER_SQUARE_METRE,
+               FOOT_LAMBERT
+       };
+
+       Luminance (cxml::ConstNodePtr node);
+
+       Luminance (float value, Unit unit);
+
+       void set_value (float v);
+       void set_unit (Unit u) {
+               _unit = u;
+       }
+
+       float value () const {
+               return _value;
+       }
+
+       Unit unit () const {
+               return _unit;
+       }
+
+       void as_xml (xmlpp::Element* parent, std::string ns) const;
+
+       static std::string unit_to_string (Unit u);
+       static Unit string_to_unit (std::string u);
+
+private:
+       float _value;
+       Unit _unit;
+};
+
+bool operator== (Luminance const& a, Luminance const& b);
+
+
+class MainSoundConfiguration
+{
+public:
+       enum Field {
+               FIVE_POINT_ONE,
+               SEVEN_POINT_ONE,
+       };
+
+       MainSoundConfiguration (std::string);
+       MainSoundConfiguration (Field field_, int channels);
+
+       Field field () const {
+               return _field;
+       }
+
+       int channels () const {
+               return _channels.size();
+       }
+
+       boost::optional<Channel> mapping (int index) const;
+       void set_mapping (int index, Channel channel);
+
+       std::string to_string () const;
+
+private:
+       Field _field;
+       std::vector<boost::optional<Channel> > _channels;
+};
+
+
 }
 
 #endif
diff --git a/test/cpl_metadata_test.cc b/test/cpl_metadata_test.cc
new file mode 100644 (file)
index 0000000..216ef60
--- /dev/null
@@ -0,0 +1,419 @@
+/*
+    Copyright (C) 2020 Carl Hetherington <cth@carlh.net>
+
+    This file is part of libdcp.
+
+    libdcp is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    libdcp is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with libdcp.  If not, see <http://www.gnu.org/licenses/>.
+
+    In addition, as a special exception, the copyright holders give
+    permission to link the code of portions of this program with the
+    OpenSSL library under certain conditions as described in each
+    individual source file, and distribute linked combinations
+    including the two.
+
+    You must obey the GNU General Public License in all respects
+    for all of the code used other than OpenSSL.  If you modify
+    file(s) with this exception, you may extend this exception to your
+    version of the file(s), but you are not obligated to do so.  If you
+    do not wish to do so, delete this exception statement from your
+    version.  If you delete this exception statement from all source
+    files in the program, then also delete it here.
+*/
+
+
+#include "certificate_chain.h"
+#include "cpl.h"
+#include "exceptions.h"
+#include "language_tag.h"
+#include "reel.h"
+#include "reel_subtitle_asset.h"
+#include "test.h"
+#include <boost/shared_ptr.hpp>
+#include <boost/test/unit_test.hpp>
+
+
+using std::list;
+using std::string;
+using std::vector;
+using boost::shared_ptr;
+
+
+BOOST_AUTO_TEST_CASE (cpl_metadata_bad_values_test)
+{
+       dcp::CPL cpl("", dcp::FEATURE);
+       BOOST_CHECK_THROW (cpl.set_version_number(-1), dcp::BadSettingError);
+
+       vector<dcp::ContentVersion> cv;
+       cv.push_back (dcp::ContentVersion("same-id", "version 1"));
+       cv.push_back (dcp::ContentVersion("same-id", "version 2"));
+       BOOST_CHECK_THROW (cpl.set_content_versions(cv), dcp::DuplicateIdError);
+}
+
+
+BOOST_AUTO_TEST_CASE (main_sound_configuration_test1)
+{
+       dcp::MainSoundConfiguration msc("51/L,R,C,LFE,-,-");
+       BOOST_CHECK_EQUAL (msc.to_string(), "51/L,R,C,LFE,-,-");
+       BOOST_CHECK_EQUAL (msc.channels(), 6);
+       BOOST_CHECK_EQUAL (msc.field(), dcp::MainSoundConfiguration::FIVE_POINT_ONE);
+       BOOST_CHECK_EQUAL (msc.mapping(0).get(), dcp::LEFT);
+       BOOST_CHECK_EQUAL (msc.mapping(1).get(), dcp::RIGHT);
+       BOOST_CHECK_EQUAL (msc.mapping(2).get(), dcp::CENTRE);
+       BOOST_CHECK_EQUAL (msc.mapping(3).get(), dcp::LFE);
+       BOOST_CHECK (!msc.mapping(4));
+       BOOST_CHECK (!msc.mapping(5));
+}
+
+
+BOOST_AUTO_TEST_CASE (main_sound_configuration_test2)
+{
+       dcp::MainSoundConfiguration msc("71/L,R,C,LFE,-,-");
+       BOOST_CHECK_EQUAL (msc.to_string(), "71/L,R,C,LFE,-,-");
+       BOOST_CHECK_EQUAL (msc.channels(), 6);
+       BOOST_CHECK_EQUAL (msc.field(), dcp::MainSoundConfiguration::SEVEN_POINT_ONE);
+       BOOST_CHECK_EQUAL (msc.mapping(0).get(), dcp::LEFT);
+       BOOST_CHECK_EQUAL (msc.mapping(1).get(), dcp::RIGHT);
+       BOOST_CHECK_EQUAL (msc.mapping(2).get(), dcp::CENTRE);
+       BOOST_CHECK_EQUAL (msc.mapping(3).get(), dcp::LFE);
+       BOOST_CHECK (!msc.mapping(4));
+       BOOST_CHECK (!msc.mapping(5));
+}
+
+
+BOOST_AUTO_TEST_CASE (main_sound_configuration_test3)
+{
+       dcp::MainSoundConfiguration msc("71/L,-,C,LFE,Lss,Rss");
+       BOOST_CHECK_EQUAL (msc.to_string(), "71/L,-,C,LFE,Lss,Rss");
+       BOOST_CHECK_EQUAL (msc.channels(), 6);
+       BOOST_CHECK_EQUAL (msc.field(), dcp::MainSoundConfiguration::SEVEN_POINT_ONE);
+       BOOST_CHECK_EQUAL (msc.mapping(0).get(), dcp::LEFT);
+       BOOST_CHECK (!msc.mapping(1));
+       BOOST_CHECK_EQUAL (msc.mapping(2).get(), dcp::CENTRE);
+       BOOST_CHECK_EQUAL (msc.mapping(3).get(), dcp::LFE);
+       BOOST_CHECK_EQUAL (msc.mapping(4).get(), dcp::LS);
+       BOOST_CHECK_EQUAL (msc.mapping(5).get(), dcp::RS);
+}
+
+
+BOOST_AUTO_TEST_CASE (main_sound_configuration_test4)
+{
+       dcp::MainSoundConfiguration msc("71/L,-,C,LFE,Lss,Rss,-,-,-,-,-,-,-,-,-");
+       BOOST_CHECK_EQUAL (msc.to_string(), "71/L,-,C,LFE,Lss,Rss,-,-,-,-,-,-,-,-,-");
+       BOOST_CHECK_EQUAL (msc.channels(), 15);
+       BOOST_CHECK_EQUAL (msc.field(), dcp::MainSoundConfiguration::SEVEN_POINT_ONE);
+       BOOST_CHECK_EQUAL (msc.mapping(0).get(), dcp::LEFT);
+       BOOST_CHECK (!msc.mapping(1));
+       BOOST_CHECK_EQUAL (msc.mapping(2).get(), dcp::CENTRE);
+       BOOST_CHECK_EQUAL (msc.mapping(3).get(), dcp::LFE);
+       BOOST_CHECK_EQUAL (msc.mapping(4).get(), dcp::LS);
+       BOOST_CHECK_EQUAL (msc.mapping(5).get(), dcp::RS);
+       for (int i = 6; i < 15; ++i) {
+               BOOST_CHECK (!msc.mapping(i));
+       }
+}
+
+
+BOOST_AUTO_TEST_CASE (main_sound_configuration_test5)
+{
+       dcp::MainSoundConfiguration msc("71/L,-,C,LFE,Lss,Rss,HI,VIN,-,-,Lrs,Rrs,DBOX,Sync,Sign");
+       BOOST_CHECK_EQUAL (msc.to_string(), "71/L,-,C,LFE,Lss,Rss,HI,VIN,-,-,Lrs,Rrs,DBOX,Sync,Sign");
+       BOOST_CHECK_EQUAL (msc.channels(), 15);
+       BOOST_CHECK_EQUAL (msc.field(), dcp::MainSoundConfiguration::SEVEN_POINT_ONE);
+       BOOST_CHECK_EQUAL (msc.mapping(0).get(), dcp::LEFT);
+       BOOST_CHECK (!msc.mapping(1));
+       BOOST_CHECK_EQUAL (msc.mapping(2).get(), dcp::CENTRE);
+       BOOST_CHECK_EQUAL (msc.mapping(3).get(), dcp::LFE);
+       BOOST_CHECK_EQUAL (msc.mapping(4).get(), dcp::LS);
+       BOOST_CHECK_EQUAL (msc.mapping(5).get(), dcp::RS);
+       BOOST_CHECK_EQUAL (msc.mapping(6).get(), dcp::HI);
+       BOOST_CHECK_EQUAL (msc.mapping(7).get(), dcp::VI);
+       BOOST_CHECK (!msc.mapping(8));
+       BOOST_CHECK (!msc.mapping(9));
+       BOOST_CHECK_EQUAL (msc.mapping(10).get(), dcp::BSL);
+       BOOST_CHECK_EQUAL (msc.mapping(11).get(), dcp::BSR);
+       BOOST_CHECK_EQUAL (msc.mapping(12).get(), dcp::MOTION_DATA);
+       BOOST_CHECK_EQUAL (msc.mapping(13).get(), dcp::SYNC_SIGNAL);
+       BOOST_CHECK_EQUAL (msc.mapping(14).get(), dcp::SIGN_LANGUAGE);
+}
+
+
+BOOST_AUTO_TEST_CASE (luminance_test1)
+{
+       BOOST_CHECK_NO_THROW (dcp::Luminance(4, dcp::Luminance::CANDELA_PER_SQUARE_METRE));
+       BOOST_CHECK_THROW (dcp::Luminance(-4, dcp::Luminance::CANDELA_PER_SQUARE_METRE), dcp::MiscError);
+}
+
+
+BOOST_AUTO_TEST_CASE (luminance_test2)
+{
+       shared_ptr<cxml::Document> doc (new cxml::Document("Luminance"));
+
+       doc->read_string (
+               "<Luminance units=\"candela-per-square-metre\">4.5</Luminance>"
+               );
+
+       dcp::Luminance lum (doc);
+       BOOST_CHECK (lum.unit() == dcp::Luminance::CANDELA_PER_SQUARE_METRE);
+       BOOST_CHECK_CLOSE (lum.value(), 4.5, 0.1);
+}
+
+
+BOOST_AUTO_TEST_CASE (luminance_test3)
+{
+       shared_ptr<cxml::Document> doc (new cxml::Document("Luminance"));
+
+       doc->read_string (
+               "<Luminance units=\"candela-per-square-motre\">4.5</Luminance>"
+               );
+
+       BOOST_CHECK_THROW (new dcp::Luminance(doc), dcp::XMLError);
+}
+
+
+BOOST_AUTO_TEST_CASE (luminance_test4)
+{
+       shared_ptr<cxml::Document> doc (new cxml::Document("Luminance"));
+
+       doc->read_string (
+               "<Luminance units=\"candela-per-square-metre\">-4.5</Luminance>"
+               );
+
+       /* We tolerate out-of-range values when reading from XML */
+       dcp::Luminance lum (doc);
+       BOOST_CHECK (lum.unit() == dcp::Luminance::CANDELA_PER_SQUARE_METRE);
+       BOOST_CHECK_CLOSE (lum.value(), -4.5, 0.1);
+}
+
+
+/** A test where most CPL metadata is present */
+BOOST_AUTO_TEST_CASE (cpl_metadata_read_test1)
+{
+       dcp::CPL cpl("test/ref/cpl_metadata_test1.xml");
+
+       BOOST_CHECK_EQUAL (cpl.full_content_title_text().get(), "full-content-title");
+       BOOST_CHECK (cpl.full_content_title_text_language().get() == "de");
+       BOOST_CHECK (cpl.release_territory().get() == "ES");
+       BOOST_CHECK_EQUAL (cpl.version_number().get(), 2);
+       BOOST_CHECK_EQUAL (cpl.status().get(), dcp::FINAL);
+       BOOST_CHECK_EQUAL (cpl.chain().get(), "the-chain");
+       BOOST_CHECK_EQUAL (cpl.distributor().get(), "the-distributor");
+       BOOST_CHECK_EQUAL (cpl.facility().get(), "the-facility");
+       BOOST_CHECK (cpl.luminance() == dcp::Luminance(4.5, dcp::Luminance::FOOT_LAMBERT));
+
+       dcp::MainSoundConfiguration msc(cpl.main_sound_configuration().get());
+       BOOST_CHECK_EQUAL (msc.mapping(0).get(), dcp::LEFT);
+       BOOST_CHECK_EQUAL (msc.mapping(1).get(), dcp::RIGHT);
+       BOOST_CHECK_EQUAL (msc.mapping(2).get(), dcp::CENTRE);
+       BOOST_CHECK_EQUAL (msc.mapping(3).get(), dcp::LFE);
+       BOOST_CHECK (!msc.mapping(4));
+       BOOST_CHECK (!msc.mapping(5));
+       BOOST_CHECK (!msc.mapping(6));
+       BOOST_CHECK (!msc.mapping(7));
+       BOOST_CHECK (!msc.mapping(8));
+       BOOST_CHECK (!msc.mapping(9));
+       BOOST_CHECK (!msc.mapping(10));
+       BOOST_CHECK (!msc.mapping(11));
+       BOOST_CHECK (!msc.mapping(12));
+       BOOST_CHECK_EQUAL (msc.mapping(13).get(), dcp::SYNC_SIGNAL);
+
+       BOOST_CHECK_EQUAL (cpl.main_sound_sample_rate().get(), 48000);
+       BOOST_CHECK (cpl.main_picture_stored_area().get() == dcp::Size(1998, 1080));
+       BOOST_CHECK (cpl.main_picture_active_area().get() == dcp::Size(1440, 1080));
+
+       list<shared_ptr<dcp::Reel> > reels = cpl.reels ();
+       BOOST_REQUIRE_EQUAL (reels.size(), 1);
+       BOOST_CHECK_EQUAL (reels.front()->main_subtitle()->language().get(), dcp::LanguageTag("de-DE"));
+
+       vector<string> asl = cpl.additional_subtitle_languages();
+       BOOST_REQUIRE_EQUAL (asl.size(), 2);
+       BOOST_CHECK_EQUAL (asl[0], "en-US");
+       BOOST_CHECK_EQUAL (asl[1], "fr-ZA");
+
+       BOOST_CHECK (cpl.additional_subtitle_languages() == asl);
+}
+
+
+/** A test where most CPL metadata is present */
+BOOST_AUTO_TEST_CASE (cpl_metadata_write_test1)
+{
+       RNGFixer fix;
+
+       dcp::CPL cpl("", dcp::FEATURE);
+       cpl.set_issue_date ("2020-08-28T13:35:06+02:00");
+
+       vector<dcp::ContentVersion> cv;
+       cv.push_back (dcp::ContentVersion("some-id", "version 1"));
+       cv.push_back (dcp::ContentVersion("another-id", "version 2"));
+       cpl.set_content_versions (cv);
+
+       cpl.set_full_content_title_text ("full-content-title");
+       cpl.set_full_content_title_text_language (dcp::LanguageTag("de"));
+       cpl.set_release_territory (dcp::LanguageTag::RegionSubtag("ES"));
+       cpl.set_version_number (2);
+       cpl.set_status (dcp::FINAL);
+       cpl.set_chain ("the-chain");
+       cpl.set_distributor ("the-distributor");
+       cpl.set_facility ("the-facility");
+       cpl.set_luminance (dcp::Luminance(4.5, dcp::Luminance::FOOT_LAMBERT));
+
+       dcp::MainSoundConfiguration msc(dcp::MainSoundConfiguration::SEVEN_POINT_ONE, 16);
+       msc.set_mapping (0, dcp::LEFT);
+       msc.set_mapping (1, dcp::RIGHT);
+       msc.set_mapping (2, dcp::CENTRE);
+       msc.set_mapping (3, dcp::LFE);
+       msc.set_mapping (13, dcp::SYNC_SIGNAL);
+       cpl.set_main_sound_configuration (msc.to_string());
+
+       cpl.set_main_sound_sample_rate (48000);
+       cpl.set_main_picture_stored_area (dcp::Size(1998, 1080));
+       cpl.set_main_picture_active_area (dcp::Size(1440, 1080));
+
+       shared_ptr<cxml::Document> doc (new cxml::Document("MainSubtitle"));
+
+       doc->read_string (
+               "<MainSubtitle>"
+               "<Id>urn:uuid:8bca1489-aab1-9259-a4fd-8150abc1de12</Id>"
+               "<AnnotationText>Goodbye world!</AnnotationText>"
+               "<EditRate>25 1</EditRate>"
+               "<IntrinsicDuration>1870</IntrinsicDuration>"
+               "<EntryPoint>0</EntryPoint>"
+               "<Duration>525</Duration>"
+               "<KeyId>urn:uuid:540cbf10-ab14-0233-ab1f-fb31501cabfa</KeyId>"
+               "<Hash>3EABjX9BB1CAWhLUtHhrGSyLgOY=</Hash>"
+               "<Language>de-DE</Language>"
+               "</MainSubtitle>"
+               );
+
+       shared_ptr<dcp::Reel> reel(new dcp::Reel());
+       reel->add (black_picture_asset("build/test/cpl_metadata_write_test1"));
+       reel->add (shared_ptr<dcp::ReelSubtitleAsset>(new dcp::ReelSubtitleAsset(doc)));
+       cpl.add (reel);
+
+       vector<dcp::LanguageTag> lt;
+       lt.push_back(dcp::LanguageTag("en-US"));
+       lt.push_back(dcp::LanguageTag("fr-ZA"));
+       cpl.set_additional_subtitle_languages (lt);
+
+       cpl.write_xml ("build/test/cpl_metadata_write_test1.xml", dcp::SMPTE, shared_ptr<dcp::CertificateChain>());
+       check_xml (
+               dcp::file_to_string("test/ref/cpl_metadata_test1.xml"),
+               dcp::file_to_string("build/test/cpl_metadata_write_test1.xml"),
+               list<string>()
+               );
+}
+
+
+/** A test where most CPL metadata is present */
+BOOST_AUTO_TEST_CASE (cpl_metadata_roundtrip_test_1)
+{
+       dcp::CPL cpl ("test/ref/cpl_metadata_test1.xml");
+       cpl.write_xml ("build/test/cpl_metadata_roundtrip_test1.xml", dcp::SMPTE, shared_ptr<dcp::CertificateChain>());
+       list<string> ignore;
+       ignore.push_back ("Id");
+       check_xml (
+               dcp::file_to_string("test/ref/cpl_metadata_test1.xml"),
+               dcp::file_to_string("build/test/cpl_metadata_roundtrip_test1.xml"),
+               ignore
+               );
+}
+
+
+/** A test where only a bare minimum of CPL metadata is present */
+BOOST_AUTO_TEST_CASE (cpl_metadata_write_test2)
+{
+       RNGFixer fix;
+
+       dcp::CPL cpl("", dcp::FEATURE);
+       cpl.set_issue_date ("2020-08-28T13:35:06+02:00");
+       cpl.set_content_version (dcp::ContentVersion("id", "version"));
+
+       dcp::MainSoundConfiguration msc(dcp::MainSoundConfiguration::SEVEN_POINT_ONE, 16);
+       msc.set_mapping (0, dcp::LEFT);
+       msc.set_mapping (1, dcp::RIGHT);
+       msc.set_mapping (2, dcp::CENTRE);
+       msc.set_mapping (3, dcp::LFE);
+       msc.set_mapping (13, dcp::SYNC_SIGNAL);
+       cpl.set_main_sound_configuration (msc.to_string());
+
+       cpl.set_main_sound_sample_rate (48000);
+       cpl.set_main_picture_stored_area (dcp::Size(1998, 1080));
+       cpl.set_main_picture_active_area (dcp::Size(1440, 1080));
+
+       shared_ptr<dcp::Reel> reel(new dcp::Reel());
+       reel->add (black_picture_asset("build/test/cpl_metadata_write_test1"));
+       cpl.add (reel);
+
+       cpl.write_xml ("build/test/cpl_metadata_write_test2.xml", dcp::SMPTE, shared_ptr<dcp::CertificateChain>());
+       check_xml (
+               dcp::file_to_string("test/ref/cpl_metadata_test2.xml"),
+               dcp::file_to_string("build/test/cpl_metadata_write_test2.xml"),
+               list<string>()
+               );
+}
+
+
+/** A test where only a bare minimum of CPL metadata is present */
+BOOST_AUTO_TEST_CASE (cpl_metadata_read_test2)
+{
+       dcp::CPL cpl("test/ref/cpl_metadata_test2.xml");
+
+       BOOST_CHECK_EQUAL (cpl.full_content_title_text().get(), "");
+       BOOST_CHECK (!cpl.full_content_title_text_language());
+       BOOST_CHECK (!cpl.release_territory());
+       BOOST_CHECK (!cpl.version_number());
+       BOOST_CHECK (!cpl.status());
+       BOOST_CHECK (!cpl.chain());
+       BOOST_CHECK (!cpl.distributor());
+       BOOST_CHECK (!cpl.facility());
+       BOOST_CHECK (!cpl.luminance());
+
+       dcp::MainSoundConfiguration msc(cpl.main_sound_configuration().get());
+       BOOST_CHECK_EQUAL (msc.mapping(0).get(), dcp::LEFT);
+       BOOST_CHECK_EQUAL (msc.mapping(1).get(), dcp::RIGHT);
+       BOOST_CHECK_EQUAL (msc.mapping(2).get(), dcp::CENTRE);
+       BOOST_CHECK_EQUAL (msc.mapping(3).get(), dcp::LFE);
+       BOOST_CHECK (!msc.mapping(4));
+       BOOST_CHECK (!msc.mapping(5));
+       BOOST_CHECK (!msc.mapping(6));
+       BOOST_CHECK (!msc.mapping(7));
+       BOOST_CHECK (!msc.mapping(8));
+       BOOST_CHECK (!msc.mapping(9));
+       BOOST_CHECK (!msc.mapping(10));
+       BOOST_CHECK (!msc.mapping(11));
+       BOOST_CHECK (!msc.mapping(12));
+       BOOST_CHECK_EQUAL (msc.mapping(13).get(), dcp::SYNC_SIGNAL);
+
+       BOOST_CHECK_EQUAL (cpl.main_sound_sample_rate().get(), 48000);
+       BOOST_CHECK (cpl.main_picture_stored_area().get() == dcp::Size(1998, 1080));
+       BOOST_CHECK (cpl.main_picture_active_area().get() == dcp::Size(1440, 1080));
+
+       list<shared_ptr<dcp::Reel> > reels = cpl.reels ();
+       BOOST_REQUIRE_EQUAL (reels.size(), 1);
+}
+
+
+/** A test where only a bare minimum of CPL metadata is present */
+BOOST_AUTO_TEST_CASE (cpl_metadata_roundtrip_test_2)
+{
+       dcp::CPL cpl ("test/ref/cpl_metadata_test2.xml");
+       cpl.write_xml ("build/test/cpl_metadata_roundtrip_test2.xml", dcp::SMPTE, shared_ptr<dcp::CertificateChain>());
+       list<string> ignore;
+       ignore.push_back ("Id");
+       check_xml (
+               dcp::file_to_string("test/ref/cpl_metadata_test2.xml"),
+               dcp::file_to_string("build/test/cpl_metadata_roundtrip_test2.xml"),
+               ignore
+               );
+}
+
index 6c701297c1748953893faf29ef1b9cb690762367..45736a7a34270fee6c4d32253b88d8aa27249fd3 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2015-2019 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2015-2020 Carl Hetherington <cth@carlh.net>
 
     This file is part of libdcp.
 
 */
 
 #include "reel_mono_picture_asset.h"
+#include "reel_subtitle_asset.h"
 #include <libcxml/cxml.h>
 #include <boost/test/unit_test.hpp>
+#include "test.h"
 
+
+using std::string;
+using boost::optional;
 using boost::shared_ptr;
 
+
 /** Test the XML constructor of ReelPictureAsset */
 BOOST_AUTO_TEST_CASE (reel_picture_asset_test)
 {
@@ -45,11 +51,11 @@ BOOST_AUTO_TEST_CASE (reel_picture_asset_test)
        doc->read_string (
                "<MainPicture>"
                "<Id>urn:uuid:06ac1ca7-9c46-4107-8864-a6448e24b04b</Id>"
-               "<AnnotationText></AnnotationText>"
+               "<AnnotationText>Hello world!</AnnotationText>"
                "<EditRate>24 1</EditRate>"
                "<IntrinsicDuration>187048</IntrinsicDuration>"
-               "<EntryPoint>0</EntryPoint>"
-               "<Duration>187048</Duration>"
+               "<EntryPoint>42</EntryPoint>"
+               "<Duration>9444</Duration>"
                "<Hash>6EQX4NjG8vxIWhLUtHhrGSyLgOY=</Hash>"
                "<FrameRate>24 1</FrameRate>"
                "<ScreenAspectRatio>2048 858</ScreenAspectRatio>"
@@ -57,5 +63,45 @@ BOOST_AUTO_TEST_CASE (reel_picture_asset_test)
                );
 
        dcp::ReelMonoPictureAsset pa (doc);
-       BOOST_CHECK_EQUAL (pa.screen_aspect_ratio(), dcp::Fraction (2048, 858));
+       BOOST_CHECK_EQUAL (pa.id(), "06ac1ca7-9c46-4107-8864-a6448e24b04b");
+       BOOST_CHECK_EQUAL (pa.annotation_text(), "Hello world!");
+       BOOST_CHECK_EQUAL (pa.edit_rate(), dcp::Fraction(24, 1));
+       BOOST_CHECK_EQUAL (pa.intrinsic_duration(), 187048);
+       BOOST_CHECK_EQUAL (pa.entry_point().get(), 42L);
+       BOOST_CHECK_EQUAL (pa.duration().get(), 9444L);
+       BOOST_CHECK_EQUAL (pa.hash().get(), string("6EQX4NjG8vxIWhLUtHhrGSyLgOY="));
+       BOOST_CHECK_EQUAL (pa.frame_rate(), dcp::Fraction(24, 1));
+       BOOST_CHECK_EQUAL (pa.screen_aspect_ratio(), dcp::Fraction(2048, 858));
+}
+
+
+/** Test the XML constructor of ReelSubtitleAsset */
+BOOST_AUTO_TEST_CASE (reel_subtitle_asset_test)
+{
+       shared_ptr<cxml::Document> doc (new cxml::Document("MainSubtitle"));
+
+       doc->read_string (
+               "<MainSubtitle>"
+               "<Id>urn:uuid:8bca1489-aab1-9259-a4fd-8150abc1de12</Id>"
+               "<AnnotationText>Goodbye world!</AnnotationText>"
+               "<EditRate>25 1</EditRate>"
+               "<IntrinsicDuration>1870</IntrinsicDuration>"
+               "<EntryPoint>0</EntryPoint>"
+               "<Duration>525</Duration>"
+               "<KeyId>urn:uuid:540cbf10-ab14-0233-ab1f-fb31501cabfa</KeyId>"
+               "<Hash>3EABjX9BB1CAWhLUtHhrGSyLgOY=</Hash>"
+               "<Language>de-DE</Language>"
+               "</MainSubtitle>"
+               );
+
+       dcp::ReelSubtitleAsset ps (doc);
+       BOOST_CHECK_EQUAL (ps.id(), "8bca1489-aab1-9259-a4fd-8150abc1de12");
+       BOOST_CHECK_EQUAL (ps.annotation_text(), "Goodbye world!");
+       BOOST_CHECK_EQUAL (ps.edit_rate(), dcp::Fraction(25, 1));
+       BOOST_CHECK_EQUAL (ps.intrinsic_duration(), 1870);
+       BOOST_CHECK_EQUAL (ps.entry_point().get(), 0L);
+       BOOST_CHECK_EQUAL (ps.duration().get(), 525L);
+       BOOST_CHECK_EQUAL (ps.hash().get(), string("3EABjX9BB1CAWhLUtHhrGSyLgOY="));
+       BOOST_REQUIRE (ps.language());
+       BOOST_CHECK_EQUAL (ps.language()->to_string(), "de-DE");
 }
diff --git a/test/ref/cpl_metadata_test1.xml b/test/ref/cpl_metadata_test1.xml
new file mode 100644 (file)
index 0000000..b734edb
--- /dev/null
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/429-7/2006/CPL">
+  <Id>urn:uuid:81fb54df-e1bf-4647-8788-ea7ba154375b</Id>
+  <AnnotationText></AnnotationText>
+  <IssueDate>2020-08-28T13:35:06+02:00</IssueDate>
+  <Issuer>libdcp1.6.4devel</Issuer>
+  <Creator>libdcp1.6.4devel</Creator>
+  <ContentTitleText></ContentTitleText>
+  <ContentKind>feature</ContentKind>
+  <ContentVersion>
+    <Id>some-id</Id>
+    <LabelText>version 1</LabelText>
+  </ContentVersion>
+  <RatingList/>
+  <ReelList>
+    <Reel>
+      <Id>urn:uuid:8b92bcee-62fc-4a33-a51a-816e9611ce85</Id>
+      <AssetList>
+        <MainPicture>
+          <Id>urn:uuid:46c3eb45-15e5-47d6-8684-d8641e4dc516</Id>
+          <AnnotationText></AnnotationText>
+          <EditRate>24 1</EditRate>
+          <IntrinsicDuration>24</IntrinsicDuration>
+          <EntryPoint>0</EntryPoint>
+          <Duration>24</Duration>
+          <Hash>BM4qh04HOSGF5vop4mhJBE7C4M0=</Hash>
+          <FrameRate>24 1</FrameRate>
+          <ScreenAspectRatio>1998 1080</ScreenAspectRatio>
+        </MainPicture>
+        <MainSubtitle>
+          <Id>urn:uuid:8bca1489-aab1-9259-a4fd-8150abc1de12</Id>
+          <AnnotationText>Goodbye world!</AnnotationText>
+          <EditRate>25 1</EditRate>
+          <IntrinsicDuration>1870</IntrinsicDuration>
+          <EntryPoint>0</EntryPoint>
+          <Duration>525</Duration>
+          <KeyId>urn:uuid:540cbf10-ab14-0233-ab1f-fb31501cabfa</KeyId>
+          <Hash>3EABjX9BB1CAWhLUtHhrGSyLgOY=</Hash>
+          <Language>de-DE</Language>
+        </MainSubtitle>
+        <meta:CompositionMetadataAsset xmlns:meta="http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata">
+          <Id>urn:uuid:77e1fb48-ce0c-4d29-bf88-8c3bfec8013a</Id>
+          <EditRate>24 1</EditRate>
+          <IntrinsicDuration>24</IntrinsicDuration>
+          <meta:FullContentTitleText language="de">full-content-title</meta:FullContentTitleText>
+          <meta:ReleaseTerritory>ES</meta:ReleaseTerritory>
+          <meta:VersionNumber status="final">2</meta:VersionNumber>
+          <meta:Chain>the-chain</meta:Chain>
+          <meta:Distributor>the-distributor</meta:Distributor>
+          <meta:Facility>the-facility</meta:Facility>
+          <meta:AlternateContentVersionList>
+            <ContentVersion>
+              <Id>another-id</Id>
+              <LabelText>version 2</LabelText>
+            </ContentVersion>
+          </meta:AlternateContentVersionList>
+          <meta:Luminance units="foot-lambert">4.5</meta:Luminance>
+          <meta:MainSoundConfiguration>71/L,R,C,LFE,-,-,-,-,-,-,-,-,-,Sync,-,-</meta:MainSoundConfiguration>
+          <meta:MainSoundSampleRate>48000 1</meta:MainSoundSampleRate>
+          <meta:MainPictureStoredArea>
+            <meta:Width>1998</meta:Width>
+            <meta:Height>1080</meta:Height>
+          </meta:MainPictureStoredArea>
+          <meta:MainPictureActiveArea>
+            <meta:Width>1440</meta:Width>
+            <meta:Height>1080</meta:Height>
+          </meta:MainPictureActiveArea>
+          <MainSubtitleLanguageList>de-DE en-US fr-ZA</MainSubtitleLanguageList>
+        </meta:CompositionMetadataAsset>
+      </AssetList>
+    </Reel>
+  </ReelList>
+</CompositionPlaylist>
diff --git a/test/ref/cpl_metadata_test2.xml b/test/ref/cpl_metadata_test2.xml
new file mode 100644 (file)
index 0000000..d61a711
--- /dev/null
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/429-7/2006/CPL">
+  <Id>urn:uuid:81fb54df-e1bf-4647-8788-ea7ba154375b</Id>
+  <AnnotationText></AnnotationText>
+  <IssueDate>2020-08-28T13:35:06+02:00</IssueDate>
+  <Issuer>libdcp1.6.4devel</Issuer>
+  <Creator>libdcp1.6.4devel</Creator>
+  <ContentTitleText></ContentTitleText>
+  <ContentKind>feature</ContentKind>
+  <ContentVersion>
+    <Id>id</Id>
+    <LabelText>version</LabelText>
+  </ContentVersion>
+  <RatingList/>
+  <ReelList>
+    <Reel>
+      <Id>urn:uuid:8b92bcee-62fc-4a33-a51a-816e9611ce85</Id>
+      <AssetList>
+        <MainPicture>
+          <Id>urn:uuid:46c3eb45-15e5-47d6-8684-d8641e4dc516</Id>
+          <AnnotationText></AnnotationText>
+          <EditRate>24 1</EditRate>
+          <IntrinsicDuration>24</IntrinsicDuration>
+          <EntryPoint>0</EntryPoint>
+          <Duration>24</Duration>
+          <Hash>BM4qh04HOSGF5vop4mhJBE7C4M0=</Hash>
+          <FrameRate>24 1</FrameRate>
+          <ScreenAspectRatio>1998 1080</ScreenAspectRatio>
+        </MainPicture>
+        <meta:CompositionMetadataAsset xmlns:meta="http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata">
+          <Id>urn:uuid:77e1fb48-ce0c-4d29-bf88-8c3bfec8013a</Id>
+          <EditRate>24 1</EditRate>
+          <IntrinsicDuration>24</IntrinsicDuration>
+          <meta:FullContentTitleText/>
+          <meta:MainSoundConfiguration>71/L,R,C,LFE,-,-,-,-,-,-,-,-,-,Sync,-,-</meta:MainSoundConfiguration>
+          <meta:MainSoundSampleRate>48000 1</meta:MainSoundSampleRate>
+          <meta:MainPictureStoredArea>
+            <meta:Width>1998</meta:Width>
+            <meta:Height>1080</meta:Height>
+          </meta:MainPictureStoredArea>
+          <meta:MainPictureActiveArea>
+            <meta:Width>1440</meta:Width>
+            <meta:Height>1080</meta:Height>
+          </meta:MainPictureActiveArea>
+        </meta:CompositionMetadataAsset>
+      </AssetList>
+    </Reel>
+  </ReelList>
+</CompositionPlaylist>
index 60dabcd4ab27737515bfce87514d9dad13cd3ddb..dd1432642d37341e0cff4549e5bf03203f1058bc 100644 (file)
 #include "sound_asset.h"
 #include "sound_asset_writer.h"
 #include "smpte_subtitle_asset.h"
+#include "mono_picture_asset.h"
+#include "openjpeg_image.h"
+#include "j2k.h"
+#include "picture_asset_writer.h"
+#include "reel_mono_picture_asset.h"
+#include "reel_asset.h"
 #include "test.h"
 #include "util.h"
 #include <asdcp/KM_util.h>
@@ -64,9 +70,11 @@ using std::list;
 using boost::shared_ptr;
 using boost::optional;
 
+
 boost::filesystem::path private_test;
 boost::filesystem::path xsd_test = "build/test/xsd with spaces";
 
+
 struct TestConfig
 {
        TestConfig()
@@ -390,4 +398,35 @@ make_simple_with_smpte_ccaps (boost::filesystem::path path)
 }
 
 
+shared_ptr<dcp::OpenJPEGImage>
+black_image ()
+{
+       shared_ptr<dcp::OpenJPEGImage> image(new dcp::OpenJPEGImage(dcp::Size(1998, 1080)));
+       int const pixels = 1998 * 1080;
+       for (int i = 0; i < 3; ++i) {
+               memset (image->data(i), 0, pixels * sizeof(int));
+       }
+       return image;
+}
+
+
+shared_ptr<dcp::ReelAsset>
+black_picture_asset (boost::filesystem::path dir, int frames)
+{
+       shared_ptr<dcp::OpenJPEGImage> image = black_image ();
+       dcp::Data frame = dcp::compress_j2k (image, 100000000, 24, false, false);
+       BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
+
+       shared_ptr<dcp::MonoPictureAsset> asset(new dcp::MonoPictureAsset(dcp::Fraction(24, 1), dcp::SMPTE));
+       boost::filesystem::create_directories (dir);
+       shared_ptr<dcp::PictureAssetWriter> writer = asset->start_write (dir / "pic.mxf", true);
+       for (int i = 0; i < frames; ++i) {
+               writer->write (frame.data().get(), frame.size());
+       }
+       writer->finalize ();
+
+       return shared_ptr<dcp::ReelAsset>(new dcp::ReelMonoPictureAsset(asset, 0));
+}
+
+
 BOOST_GLOBAL_FIXTURE (TestConfig);
index 6fc2067d638f3a8dcfedd458a1632f2f0d38fd13..95827af7607b8f0b1fe878d7cc38cba1e90b2873 100644 (file)
@@ -24,7 +24,9 @@
 #include "reel.h"
 #include "reel_subtitle_asset.h"
 #include "subtitle.h"
+#include "reel_asset.h"
 #include <boost/filesystem.hpp>
+#include <boost/optional.hpp>
 
 namespace xmlpp {
        class Element;
@@ -47,6 +49,8 @@ extern boost::shared_ptr<dcp::DCP> make_simple_with_interop_subs (boost::filesys
 extern boost::shared_ptr<dcp::DCP> make_simple_with_smpte_subs (boost::filesystem::path path);
 extern boost::shared_ptr<dcp::DCP> make_simple_with_interop_ccaps (boost::filesystem::path path);
 extern boost::shared_ptr<dcp::DCP> make_simple_with_smpte_ccaps (boost::filesystem::path path);
+extern boost::shared_ptr<dcp::OpenJPEGImage> black_image ();
+extern boost::shared_ptr<dcp::ReelAsset> black_picture_asset (boost::filesystem::path dir, int frames = 24);
 
 /** Creating an object of this class will make asdcplib's random number generation
  *  (more) predictable.
@@ -57,4 +61,3 @@ public:
        RNGFixer ();
        ~RNGFixer ();
 };
-
index f1213c10bb3715c9b7935400ebe8643087624485..14d498b06c5ac3a2657c7c35263ddd75d8af441d 100644 (file)
@@ -514,19 +514,6 @@ BOOST_AUTO_TEST_CASE (verify_test14)
 }
 
 
-static
-shared_ptr<dcp::OpenJPEGImage>
-black_image ()
-{
-       shared_ptr<dcp::OpenJPEGImage> image(new dcp::OpenJPEGImage(dcp::Size(1998, 1080)));
-       int const pixels = 1998 * 1080;
-       for (int i = 0; i < 3; ++i) {
-               memset (image->data(i), 0, pixels * sizeof(int));
-       }
-       return image;
-}
-
-
 static
 void
 dcp_from_frame (dcp::Data const& frame, boost::filesystem::path dir)
index 9958a677896d44f0b04c854c924d546a44445325..5fd2f430a2436211d786e80555e6648f0c12f7ff 100644 (file)
@@ -67,6 +67,7 @@ def build(bld):
                  colour_test.cc
                  colour_conversion_test.cc
                  combine_test.cc
+                 cpl_metadata_test.cc
                  cpl_sar_test.cc
                  cpl_ratings_test.cc
                  dcp_font_test.cc