From: Carl Hetherington Date: Thu, 27 Aug 2020 20:59:12 +0000 (+0200) Subject: Support CPL metadata. X-Git-Tag: v1.8.0~291 X-Git-Url: https://main.carlh.net/gitweb/?p=libdcp.git;a=commitdiff_plain;h=b933775fc54e0b51ad3777d72bf2866f0c04bacc Support CPL metadata. --- diff --git a/src/cpl.cc b/src/cpl.cc index 6c5610d8..8285f87e 100644 --- a/src/cpl.cc +++ b/src/cpl.cc @@ -45,8 +45,10 @@ #include "local_time.h" #include "dcp_assert.h" #include "compose.hpp" +#include "raw_convert.h" #include #include +#include #include 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 (f, "ReelList", "Reel"); + cxml::ConstNodePtr reel_list = f.node_child ("ReelList"); + if (reel_list) { + list 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) } /** 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_ptradd_child ("ReelList"); + bool first = true; BOOST_FOREACH (shared_ptr 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_ptrnode_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(vn->content()); + /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */ + optional 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 sr_bits; + boost::split (sr_bits, sr, boost::is_any_of(" ")); + DCP_ASSERT (sr_bits.size() == 2); + _main_sound_sample_rate = raw_convert(sr_bits[0]); + + _main_picture_stored_area = dcp::Size ( + node->node_child("MainPictureStoredArea")->number_child("Width"), + node->node_child("MainPictureStoredArea")->number_child("Height") + ); + + _main_picture_active_area = dcp::Size ( + node->node_child("MainPictureActiveArea")->number_child("Width"), + node->node_child("MainPictureActiveArea")->number_child("Height") + ); + + optional sll = node->optional_string_child("MainSubtitleLanguageList"); + if (sll) { + vector 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 sub = _reels.front()->main_subtitle(); + if (sub) { + optional 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 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(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(*_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(*_main_sound_sample_rate) + " 1"); + + xmlpp::Element* stored = meta->add_child("MainPictureStoredArea", "meta"); + stored->add_child("Width", "meta")->add_child_text(raw_convert(_main_picture_stored_area->width)); + stored->add_child("Height", "meta")->add_child_text(raw_convert(_main_picture_stored_area->height)); + + xmlpp::Element* active = meta->add_child("MainPictureActiveArea", "meta"); + active->add_child("Width", "meta")->add_child_text(raw_convert(_main_picture_active_area->width)); + active->add_child("Height", "meta")->add_child_text(raw_convert(_main_picture_active_area->height)); + + optional first_subtitle_language; + BOOST_FOREACH (shared_ptr 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 > 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 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 const& langs) +{ + _additional_subtitle_languages.clear (); + BOOST_FOREACH (dcp::LanguageTag const& i, langs) { + _additional_subtitle_languages.push_back (i.to_string()); + } +} diff --git a/src/cpl.h b/src/cpl.h index bb8975c9..72366ee3 100644 --- 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 #include @@ -160,6 +161,114 @@ public: _ratings = r; } + boost::optional 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 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 release_territory () const { + return _release_territory; + } + + void set_release_territory (dcp::LanguageTag::RegionSubtag t) { + _release_territory = t.subtag(); + } + + boost::optional version_number () const { + return _version_number; + } + + void set_version_number (int v); + + boost::optional status () const { + return _status; + } + + void set_status (Status s) { + _status = s; + } + + boost::optional chain () const { + return _chain; + } + + void set_chain (std::string c) { + _chain = c; + } + + boost::optional distributor () const { + return _distributor; + } + + void set_distributor (std::string d) { + _distributor = d; + } + + boost::optional facility () const { + return _facility; + } + + void set_facility (std::string f) { + _facility = f; + } + + boost::optional luminance () const { + return _luminance; + } + + void set_luminance (Luminance l) { + _luminance = l; + } + + boost::optional main_sound_configuration () const { + return _main_sound_configuration; + } + + void set_main_sound_configuration (std::string c) { + _main_sound_configuration = c; + } + + boost::optional 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 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 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 additional_subtitle_languages () const { + return _additional_subtitle_languages; + } + + void set_additional_subtitle_languages (std::vector const& lang); + boost::optional 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; ///< <ContentKind> std::vector _content_versions; std::vector _ratings; + /** Human-readable name of the composition, without any metadata (i.e. no -FTR-EN-XX- etc.) */ + boost::optional _full_content_title_text; + boost::optional _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 _release_territory; + boost::optional _version_number; + boost::optional _status; + boost::optional _chain; + boost::optional _distributor; + boost::optional _facility; + boost::optional _luminance; + boost::optional _main_sound_configuration; + boost::optional _main_sound_sample_rate; + boost::optional _main_picture_stored_area; + boost::optional _main_picture_active_area; + /* See note for _release_territory above */ + std::vector _additional_subtitle_languages; std::list > _reels; diff --git a/src/exceptions.cc b/src/exceptions.cc index 70320b52..16676e20 100644 --- a/src/exceptions.cc +++ b/src/exceptions.cc @@ -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. diff --git a/src/exceptions.h b/src/exceptions.h index 79ee9fca..12624175 100644 --- a/src/exceptions.h +++ b/src/exceptions.h @@ -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 diff --git a/src/language_tag.cc b/src/language_tag.cc index b64dab72..4ac90507 100644 --- a/src/language_tag.cc +++ b/src/language_tag.cc @@ -41,6 +41,9 @@ #include +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 -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::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 variant = find_in_list (VARIANT, i.subtag()); + optional variant = get_subtag_data (VARIANT, i.subtag()); DCP_ASSERT (variant); d += variant->description + " dialect of "; } - optional language = find_in_list (LANGUAGE, _language->subtag()); + optional language = get_subtag_data (LANGUAGE, _language->subtag()); DCP_ASSERT (language); d += language->description; if (_script) { - optional script = find_in_list (SCRIPT, _script->subtag()); + optional script = get_subtag_data (SCRIPT, _script->subtag()); DCP_ASSERT (script); d += " written using the " + script->description + " script"; } if (_region) { - optional region = find_in_list (REGION, _region->subtag()); + optional region = get_subtag_data (REGION, _region->subtag()); DCP_ASSERT (region); d += " for " + region->description; } BOOST_FOREACH (ExtlangSubtag const& i, _extlangs) { - optional extlang = find_in_list (EXTLANG, i.subtag()); + optional 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 > +LanguageTag::subtags () const +{ + vector > 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::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(); +} + + +optional +LanguageTag::get_subtag_description (LanguageTag::SubtagType type, string subtag) +{ + optional data = get_subtag_data (type, subtag); + if (!data) { + return optional(); + } + + return data->description; +} + diff --git a/src/language_tag.h b/src/language_tag.h index 8aa8a723..74b4a8a7 100644 --- a/src/language_tag.h +++ b/src/language_tag.h @@ -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 language() { + return _language; + } + void set_language (LanguageSubtag language); + + boost::optional script() const { + return _script; + } + void set_script (ScriptSubtag script); + + boost::optional region() const { + return _region; + } + void set_region (RegionSubtag region); + + std::vector variants() const { + return _variants; + } + void set_variants (std::vector variants); void add_variant (VariantSubtag variant); + + std::vector extlangs() const { + return _extlangs; + } + void set_extlangs (std::vector extlangs); void add_extlang (ExtlangSubtag extlang); + std::vector > subtags () const; + static std::vector get_all (SubtagType type); static std::string subtag_type_name (SubtagType type); + static boost::optional get_subtag_description (SubtagType, std::string subtag); + static boost::optional get_subtag_data (SubtagType, std::string subtag); + + template + static boost::optional get_subtag_description (T s) { + return get_subtag_description (s.type(), s.subtag()); + } + + template + static boost::optional get_subtag_data (T s) { + return get_subtag_data (s.type(), s.subtag()); + } + private: + boost::optional _language; boost::optional _script; boost::optional _region; @@ -163,6 +229,8 @@ private: std::vector _extlangs; }; +extern bool operator==(dcp::LanguageTag const& a, dcp::LanguageTag const& b); +extern std::ostream& operator<<(std::ostream& os, dcp::LanguageTag const& tag); } diff --git a/src/reel.cc b/src/reel.cc index 453a3163..d3e2a850 100644 --- a/src/reel.cc +++ b/src/reel.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2014-2017 Carl Hetherington + Copyright (C) 2014-2020 Carl Hetherington This file is part of libdcp. @@ -115,7 +115,7 @@ Reel::Reel (boost::shared_ptr 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 diff --git a/src/reel.h b/src/reel.h index 3792682e..ccad3c83 100644 --- a/src/reel.h +++ b/src/reel.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012-2014 Carl Hetherington + Copyright (C) 2012-2020 Carl Hetherington This file is part of libdcp. @@ -113,7 +113,7 @@ public: std::list > 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; diff --git a/src/reel_subtitle_asset.cc b/src/reel_subtitle_asset.cc index 4fa9fd0a..f7df36f8 100644 --- a/src/reel_subtitle_asset.cc +++ b/src/reel_subtitle_asset.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2012-2017 Carl Hetherington + Copyright (C) 2012-2020 Carl Hetherington 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 node) : ReelAsset (node) , ReelMXF (node) { - node->ignore_child ("Language"); + optional 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; } diff --git a/src/reel_subtitle_asset.h b/src/reel_subtitle_asset.h index 4cc03cf1..123478cf 100644 --- a/src/reel_subtitle_asset.h +++ b/src/reel_subtitle_asset.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012-2017 Carl Hetherington + Copyright (C) 2012-2020 Carl Hetherington 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 (); } + void set_language (dcp::LanguageTag language); + + boost::optional language () const { + return _language; + } + private: std::string key_type () const; std::string cpl_node_name (Standard standard) const; + + boost::optional _language; }; } diff --git a/src/types.cc b/src/types.cc index 746b6f6a..9d67ee67 100644 --- a/src/types.cc +++ b/src/types.cc @@ -38,11 +38,16 @@ #include "dcp_assert.h" #include #include +#include +#include #include +#include #include #include -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 (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(_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 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 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()); + } 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 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 +MainSoundConfiguration::mapping (int index) const +{ + DCP_ASSERT (static_cast(index) < _channels.size()); + return _channels[index]; +} + + +void +MainSoundConfiguration::set_mapping (int index, Channel c) +{ + DCP_ASSERT (static_cast(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); +} + diff --git a/src/types.h b/src/types.h index f8a9f45e..9a4b3e13 100644 --- a/src/types.h +++ b/src/types.h @@ -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 mapping (int index) const; + void set_mapping (int index, Channel channel); + + std::string to_string () const; + +private: + Field _field; + std::vector > _channels; +}; + + } #endif diff --git a/test/cpl_metadata_test.cc b/test/cpl_metadata_test.cc new file mode 100644 index 00000000..216ef600 --- /dev/null +++ b/test/cpl_metadata_test.cc @@ -0,0 +1,419 @@ +/* + Copyright (C) 2020 Carl Hetherington + + 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 . + + 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 +#include + + +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 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 doc (new cxml::Document("Luminance")); + + doc->read_string ( + "4.5" + ); + + 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 doc (new cxml::Document("Luminance")); + + doc->read_string ( + "4.5" + ); + + BOOST_CHECK_THROW (new dcp::Luminance(doc), dcp::XMLError); +} + + +BOOST_AUTO_TEST_CASE (luminance_test4) +{ + shared_ptr doc (new cxml::Document("Luminance")); + + doc->read_string ( + "-4.5" + ); + + /* 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 > reels = cpl.reels (); + BOOST_REQUIRE_EQUAL (reels.size(), 1); + BOOST_CHECK_EQUAL (reels.front()->main_subtitle()->language().get(), dcp::LanguageTag("de-DE")); + + vector 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 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 doc (new cxml::Document("MainSubtitle")); + + doc->read_string ( + "" + "urn:uuid:8bca1489-aab1-9259-a4fd-8150abc1de12" + "Goodbye world!" + "25 1" + "1870" + "0" + "525" + "urn:uuid:540cbf10-ab14-0233-ab1f-fb31501cabfa" + "3EABjX9BB1CAWhLUtHhrGSyLgOY=" + "de-DE" + "" + ); + + shared_ptr reel(new dcp::Reel()); + reel->add (black_picture_asset("build/test/cpl_metadata_write_test1")); + reel->add (shared_ptr(new dcp::ReelSubtitleAsset(doc))); + cpl.add (reel); + + vector 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()); + check_xml ( + dcp::file_to_string("test/ref/cpl_metadata_test1.xml"), + dcp::file_to_string("build/test/cpl_metadata_write_test1.xml"), + list() + ); +} + + +/** 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()); + list 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 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()); + check_xml ( + dcp::file_to_string("test/ref/cpl_metadata_test2.xml"), + dcp::file_to_string("build/test/cpl_metadata_write_test2.xml"), + list() + ); +} + + +/** 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 > 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()); + list 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 + ); +} + diff --git a/test/reel_asset_test.cc b/test/reel_asset_test.cc index 6c701297..45736a7a 100644 --- a/test/reel_asset_test.cc +++ b/test/reel_asset_test.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2015-2019 Carl Hetherington + Copyright (C) 2015-2020 Carl Hetherington This file is part of libdcp. @@ -32,11 +32,17 @@ */ #include "reel_mono_picture_asset.h" +#include "reel_subtitle_asset.h" #include #include +#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 ( "" "urn:uuid:06ac1ca7-9c46-4107-8864-a6448e24b04b" - "" + "Hello world!" "24 1" "187048" - "0" - "187048" + "42" + "9444" "6EQX4NjG8vxIWhLUtHhrGSyLgOY=" "24 1" "2048 858" @@ -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 doc (new cxml::Document("MainSubtitle")); + + doc->read_string ( + "" + "urn:uuid:8bca1489-aab1-9259-a4fd-8150abc1de12" + "Goodbye world!" + "25 1" + "1870" + "0" + "525" + "urn:uuid:540cbf10-ab14-0233-ab1f-fb31501cabfa" + "3EABjX9BB1CAWhLUtHhrGSyLgOY=" + "de-DE" + "" + ); + + 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 index 00000000..b734edbb --- /dev/null +++ b/test/ref/cpl_metadata_test1.xml @@ -0,0 +1,73 @@ + + + urn:uuid:81fb54df-e1bf-4647-8788-ea7ba154375b + + 2020-08-28T13:35:06+02:00 + libdcp1.6.4devel + libdcp1.6.4devel + + feature + + some-id + version 1 + + + + + urn:uuid:8b92bcee-62fc-4a33-a51a-816e9611ce85 + + + urn:uuid:46c3eb45-15e5-47d6-8684-d8641e4dc516 + + 24 1 + 24 + 0 + 24 + BM4qh04HOSGF5vop4mhJBE7C4M0= + 24 1 + 1998 1080 + + + urn:uuid:8bca1489-aab1-9259-a4fd-8150abc1de12 + Goodbye world! + 25 1 + 1870 + 0 + 525 + urn:uuid:540cbf10-ab14-0233-ab1f-fb31501cabfa + 3EABjX9BB1CAWhLUtHhrGSyLgOY= + de-DE + + + urn:uuid:77e1fb48-ce0c-4d29-bf88-8c3bfec8013a + 24 1 + 24 + full-content-title + ES + 2 + the-chain + the-distributor + the-facility + + + another-id + version 2 + + + 4.5 + 71/L,R,C,LFE,-,-,-,-,-,-,-,-,-,Sync,-,- + 48000 1 + + 1998 + 1080 + + + 1440 + 1080 + + de-DE en-US fr-ZA + + + + + diff --git a/test/ref/cpl_metadata_test2.xml b/test/ref/cpl_metadata_test2.xml new file mode 100644 index 00000000..d61a7113 --- /dev/null +++ b/test/ref/cpl_metadata_test2.xml @@ -0,0 +1,49 @@ + + + urn:uuid:81fb54df-e1bf-4647-8788-ea7ba154375b + + 2020-08-28T13:35:06+02:00 + libdcp1.6.4devel + libdcp1.6.4devel + + feature + + id + version + + + + + urn:uuid:8b92bcee-62fc-4a33-a51a-816e9611ce85 + + + urn:uuid:46c3eb45-15e5-47d6-8684-d8641e4dc516 + + 24 1 + 24 + 0 + 24 + BM4qh04HOSGF5vop4mhJBE7C4M0= + 24 1 + 1998 1080 + + + urn:uuid:77e1fb48-ce0c-4d29-bf88-8c3bfec8013a + 24 1 + 24 + + 71/L,R,C,LFE,-,-,-,-,-,-,-,-,-,Sync,-,- + 48000 1 + + 1998 + 1080 + + + 1440 + 1080 + + + + + + diff --git a/test/test.cc b/test/test.cc index 60dabcd4..dd143264 100644 --- a/test/test.cc +++ b/test/test.cc @@ -48,6 +48,12 @@ #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 @@ -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 +black_image () +{ + shared_ptr 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 +black_picture_asset (boost::filesystem::path dir, int frames) +{ + shared_ptr image = black_image (); + dcp::Data frame = dcp::compress_j2k (image, 100000000, 24, false, false); + BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8)); + + shared_ptr asset(new dcp::MonoPictureAsset(dcp::Fraction(24, 1), dcp::SMPTE)); + boost::filesystem::create_directories (dir); + shared_ptr 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(new dcp::ReelMonoPictureAsset(asset, 0)); +} + + BOOST_GLOBAL_FIXTURE (TestConfig); diff --git a/test/test.h b/test/test.h index 6fc2067d..95827af7 100644 --- a/test/test.h +++ b/test/test.h @@ -24,7 +24,9 @@ #include "reel.h" #include "reel_subtitle_asset.h" #include "subtitle.h" +#include "reel_asset.h" #include +#include namespace xmlpp { class Element; @@ -47,6 +49,8 @@ extern boost::shared_ptr make_simple_with_interop_subs (boost::filesys extern boost::shared_ptr make_simple_with_smpte_subs (boost::filesystem::path path); extern boost::shared_ptr make_simple_with_interop_ccaps (boost::filesystem::path path); extern boost::shared_ptr make_simple_with_smpte_ccaps (boost::filesystem::path path); +extern boost::shared_ptr black_image (); +extern boost::shared_ptr 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 (); }; - diff --git a/test/verify_test.cc b/test/verify_test.cc index f1213c10..14d498b0 100644 --- a/test/verify_test.cc +++ b/test/verify_test.cc @@ -514,19 +514,6 @@ BOOST_AUTO_TEST_CASE (verify_test14) } -static -shared_ptr -black_image () -{ - shared_ptr 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) diff --git a/test/wscript b/test/wscript index 9958a677..5fd2f430 100644 --- a/test/wscript +++ b/test/wscript @@ -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