2 Copyright (C) 2012-2021 Carl Hetherington <cth@carlh.net>
4 This file is part of libdcp.
6 libdcp is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
11 libdcp is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with libdcp. If not, see <http://www.gnu.org/licenses/>.
19 In addition, as a special exception, the copyright holders give
20 permission to link the code of portions of this program with the
21 OpenSSL library under certain conditions as described in each
22 individual source file, and distribute linked combinations
25 You must obey the GNU General Public License in all respects
26 for all of the code used other than OpenSSL. If you modify
27 file(s) with this exception, you may extend this exception to your
28 version of the file(s), but you are not obligated to do so. If you
29 do not wish to do so, delete this exception statement from your
30 version. If you delete this exception statement from all source
31 files in the program, then also delete it here.
40 #include "certificate_chain.h"
41 #include "compose.hpp"
43 #include "dcp_assert.h"
44 #include "equality_options.h"
45 #include "filesystem.h"
46 #include "local_time.h"
48 #include "raw_convert.h"
50 #include "reel_atmos_asset.h"
51 #include "reel_closed_caption_asset.h"
52 #include "reel_picture_asset.h"
53 #include "reel_sound_asset.h"
54 #include "reel_subtitle_asset.h"
59 LIBDCP_DISABLE_WARNINGS
60 #include <asdcp/Metadata.h>
61 LIBDCP_ENABLE_WARNINGS
62 #include <libxml/parser.h>
63 LIBDCP_DISABLE_WARNINGS
64 #include <libxml++/libxml++.h>
65 LIBDCP_ENABLE_WARNINGS
66 #include <boost/algorithm/string.hpp>
70 using std::dynamic_pointer_cast;
73 using std::make_shared;
76 using std::shared_ptr;
79 using boost::optional;
83 static string const cpl_interop_ns = "http://www.digicine.com/PROTO-ASDCP-CPL-20040511#";
84 static string const cpl_smpte_ns = "http://www.smpte-ra.org/schemas/429-7/2006/CPL";
85 static string const cpl_metadata_ns = "http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata";
86 static string const mca_sub_descriptors_ns = "http://isdcf.com/ns/cplmd/mca";
87 static string const smpte_395_ns = "http://www.smpte-ra.org/reg/395/2014/13/1/aaf";
88 static string const smpte_335_ns = "http://www.smpte-ra.org/reg/335/2012";
91 CPL::CPL (string annotation_text, ContentKind content_kind, Standard standard)
92 /* default _content_title_text to annotation_text */
93 : _issuer("libdcp", dcp::version)
94 , _creator("libdcp", dcp::version)
95 , _issue_date (LocalTime().as_string())
96 , _annotation_text (annotation_text)
97 , _content_title_text (annotation_text)
98 , _content_kind (content_kind)
99 , _standard (standard)
102 cv.label_text = cv.id + LocalTime().as_string();
103 _content_versions.push_back (cv);
107 CPL::CPL (boost::filesystem::path file)
109 , _content_kind (ContentKind::FEATURE)
111 cxml::Document f ("CompositionPlaylist");
112 f.read_file(dcp::filesystem::fix_long_path(file));
114 if (f.namespace_uri() == cpl_interop_ns) {
115 _standard = Standard::INTEROP;
116 } else if (f.namespace_uri() == cpl_smpte_ns) {
117 _standard = Standard::SMPTE;
119 boost::throw_exception (XMLError ("Unrecognised CPL namespace " + f.namespace_uri()));
122 _id = remove_urn_uuid (f.string_child ("Id"));
123 _annotation_text = f.optional_string_child("AnnotationText");
124 _issuer = f.optional_string_child("Issuer").get_value_or("");
125 _creator = f.optional_string_child("Creator").get_value_or("");
126 _issue_date = f.string_child ("IssueDate");
127 _content_title_text = f.string_child ("ContentTitleText");
128 auto content_kind = f.node_child("ContentKind");
129 _content_kind = ContentKind(content_kind->content(), content_kind->optional_string_attribute("scope"));
130 shared_ptr<cxml::Node> content_version = f.optional_node_child ("ContentVersion");
131 if (content_version) {
132 /* XXX: SMPTE should insist that Id is present */
133 _content_versions.push_back (
135 content_version->optional_string_child("Id").get_value_or(""),
136 content_version->string_child("LabelText")
139 content_version->done ();
140 } else if (_standard == Standard::SMPTE) {
141 /* ContentVersion is required in SMPTE */
142 throw XMLError ("Missing ContentVersion tag in CPL");
144 auto rating_list = f.node_child ("RatingList");
145 for (auto i: rating_list->node_children("Rating")) {
146 _ratings.push_back (Rating(i));
149 for (auto i: f.node_child("ReelList")->node_children("Reel")) {
150 _reels.push_back (make_shared<Reel>(i, _standard));
153 auto reel_list = f.node_child ("ReelList");
154 auto reels = reel_list->node_children("Reel");
155 if (!reels.empty()) {
156 auto asset_list = reels.front()->node_child("AssetList");
157 auto metadata = asset_list->optional_node_child("CompositionMetadataAsset");
159 read_composition_metadata_asset (metadata);
160 _read_composition_metadata = true;
164 f.ignore_child ("Issuer");
165 f.ignore_child ("Signer");
166 f.ignore_child ("Signature");
173 CPL::add (std::shared_ptr<Reel> reel)
175 _reels.push_back (reel);
180 CPL::set (std::vector<std::shared_ptr<Reel>> reels)
187 CPL::write_xml(boost::filesystem::path file, shared_ptr<const CertificateChain> signer, bool include_mca_subdescriptors) const
190 xmlpp::Element* root;
191 if (_standard == Standard::INTEROP) {
192 root = doc.create_root_node ("CompositionPlaylist", cpl_interop_ns);
194 root = doc.create_root_node ("CompositionPlaylist", cpl_smpte_ns);
197 root->add_child("Id")->add_child_text ("urn:uuid:" + _id);
198 if (_annotation_text) {
199 root->add_child("AnnotationText")->add_child_text (*_annotation_text);
201 root->add_child("IssueDate")->add_child_text (_issue_date);
202 root->add_child("Issuer")->add_child_text (_issuer);
203 root->add_child("Creator")->add_child_text (_creator);
204 root->add_child("ContentTitleText")->add_child_text (_content_title_text);
205 auto content_kind = root->add_child("ContentKind");
206 content_kind->add_child_text(_content_kind.name());
207 if (_content_kind.scope()) {
208 content_kind->set_attribute("scope", *_content_kind.scope());
210 if (_content_versions.empty()) {
214 _content_versions[0].as_xml (root);
217 auto rating_list = root->add_child("RatingList");
218 for (auto i: _ratings) {
219 i.as_xml (rating_list->add_child("Rating"));
222 auto reel_list = root->add_child ("ReelList");
224 if (_reels.empty()) {
225 throw NoReelsError ();
229 for (auto i: _reels) {
230 auto asset_list = i->write_to_cpl (reel_list, _standard);
231 if (first && _standard == Standard::SMPTE) {
232 maybe_write_composition_metadata_asset(asset_list, include_mca_subdescriptors);
240 signer->sign (root, _standard);
243 doc.write_to_file_formatted(dcp::filesystem::fix_long_path(file).string(), "UTF-8");
250 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
252 _cpl_metadata_id = remove_urn_uuid(node->string_child("Id"));
254 /* FullContentTitleText is compulsory but in DoM #2295 we saw a commercial tool which
255 * apparently didn't include it, so as usual we have to be defensive.
257 if (auto fctt = node->optional_node_child("FullContentTitleText")) {
258 _full_content_title_text = fctt->content();
259 _full_content_title_text_language = fctt->optional_string_attribute("language");
262 _release_territory = node->optional_string_child("ReleaseTerritory");
263 if (_release_territory) {
264 _release_territory_scope = node->node_child("ReleaseTerritory")->optional_string_attribute("scope");
267 auto vn = node->optional_node_child("VersionNumber");
269 _version_number = raw_convert<int>(vn->content());
270 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
271 auto vn_status = vn->optional_string_attribute("status");
273 _status = string_to_status (*vn_status);
277 _chain = node->optional_string_child("Chain");
278 _distributor = node->optional_string_child("Distributor");
279 _facility = node->optional_string_child("Facility");
281 auto acv = node->optional_node_child("AlternateContentVersionList");
283 for (auto i: acv->node_children("ContentVersion")) {
284 _content_versions.push_back (ContentVersion(i));
288 auto lum = node->optional_node_child("Luminance");
290 _luminance = Luminance (lum);
293 if (auto msc = node->optional_string_child("MainSoundConfiguration")) {
295 _main_sound_configuration = MainSoundConfiguration(*msc);
296 } catch (MainSoundConfigurationError& e) {
297 /* With Interop DCPs this node may not make any sense, but that's OK */
298 if (_standard == dcp::Standard::SMPTE) {
304 auto sr = node->optional_string_child("MainSoundSampleRate");
306 vector<string> sr_bits;
307 boost::split (sr_bits, *sr, boost::is_any_of(" "));
308 DCP_ASSERT (sr_bits.size() == 2);
309 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
312 if (_standard == dcp::Standard::SMPTE) {
313 _main_picture_stored_area = dcp::Size (
314 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
315 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
319 _main_picture_active_area = dcp::Size (
320 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
321 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
324 auto sll = node->optional_string_child("MainSubtitleLanguageList");
326 vector<string> sll_split;
327 boost::split (sll_split, *sll, boost::is_any_of(" "));
328 DCP_ASSERT (!sll_split.empty());
330 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
332 if (!_reels.empty()) {
333 auto sub = _reels.front()->main_subtitle();
335 auto lang = sub->language();
336 if (lang && lang == sll_split[0]) {
342 for (auto i = first; i < sll_split.size(); ++i) {
343 _additional_subtitle_languages.push_back (sll_split[i]);
347 auto eml = node->optional_node_child ("ExtensionMetadataList");
349 auto extension_metadata = [eml](string scope, string name, string property) -> boost::optional<std::string> {
354 for (auto i: eml->node_children("ExtensionMetadata")) {
355 auto xml_scope = i->optional_string_attribute("scope");
356 auto xml_name = i->optional_string_child("Name");
357 if (xml_scope && *xml_scope == scope && xml_name && *xml_name == name) {
358 auto property_list = i->node_child("PropertyList");
359 for (auto j: property_list->node_children("Property")) {
360 auto property_name = j->optional_string_child("Name");
361 auto property_value = j->optional_string_child("Value");
362 if (property_name && property_value && *property_name == property) {
363 return property_value;
372 _sign_language_video_language = extension_metadata("http://isdcf.com/2017/10/SignLanguageVideo", "Sign Language Video", "Language Tag");
373 _dolby_edr_image_transfer_function = extension_metadata("http://www.dolby.com/schemas/2014/EDR-Metadata", "Dolby EDR", "image transfer function");
378 CPL::write_mca_subdescriptors(xmlpp::Element* parent, shared_ptr<const SoundAsset> asset) const
380 auto reader = asset->start_read ();
381 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
382 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
383 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
384 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
387 auto mca_subs = parent->add_child("mca:MCASubDescriptors");
388 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
389 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
390 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
391 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
393 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
394 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
395 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
396 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
397 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
398 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
399 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
400 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
401 if (!soundfield->MCATagName.empty()) {
402 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
403 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
405 if (!soundfield->RFC5646SpokenLanguage.empty()) {
406 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
407 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
410 /* Find the MCA subdescriptors in the MXF so that we can also write them here */
411 list<ASDCP::MXF::InterchangeObject*> channels;
412 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
413 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
417 for (auto i: channels) {
418 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
419 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
420 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
421 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
422 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
423 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
424 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
425 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
426 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
427 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
428 if (!channel->MCATagName.empty()) {
429 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
430 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
432 if (!channel->MCAChannelID.empty()) {
433 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
435 if (!channel->RFC5646SpokenLanguage.empty()) {
436 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
437 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
439 if (!channel->SoundfieldGroupLinkID.empty()) {
440 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
441 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
448 /** Write a CompositionMetadataAsset node as a child of @param node provided
449 * the required metadata is stored in the object. If any required metadata
450 * is missing this method will do nothing.
453 CPL::maybe_write_composition_metadata_asset(xmlpp::Element* node, bool include_mca_subdescriptors) const
456 !_main_sound_configuration ||
457 !_main_sound_sample_rate ||
458 !_main_picture_stored_area ||
459 !_main_picture_active_area ||
461 !_reels.front()->main_picture()) {
465 auto meta = node->add_child("meta:CompositionMetadataAsset");
466 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
468 meta->add_child("Id")->add_child_text("urn:uuid:" + _cpl_metadata_id);
470 auto mp = _reels.front()->main_picture();
471 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
472 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
474 auto fctt = meta->add_child("FullContentTitleText", "meta");
475 if (_full_content_title_text && !_full_content_title_text->empty()) {
476 fctt->add_child_text (*_full_content_title_text);
478 if (_full_content_title_text_language) {
479 fctt->set_attribute("language", *_full_content_title_text_language);
482 if (_release_territory) {
483 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
486 if (_version_number) {
487 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
488 vn->add_child_text(raw_convert<string>(*_version_number));
490 vn->set_attribute("status", status_to_string(*_status));
495 meta->add_child("Chain", "meta")->add_child_text(*_chain);
499 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
503 meta->add_child("Facility", "meta")->add_child_text(*_facility);
506 if (_content_versions.size() > 1) {
507 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
508 for (size_t i = 1; i < _content_versions.size(); ++i) {
509 _content_versions[i].as_xml (vc);
514 _luminance->as_xml (meta, "meta");
517 if (_main_sound_configuration) {
518 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(_main_sound_configuration->to_string());
520 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
522 auto stored = meta->add_child("MainPictureStoredArea", "meta");
523 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
524 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
526 auto active = meta->add_child("MainPictureActiveArea", "meta");
527 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
528 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
530 optional<string> first_subtitle_language;
531 for (auto i: _reels) {
532 if (i->main_subtitle()) {
533 first_subtitle_language = i->main_subtitle()->language();
534 if (first_subtitle_language) {
540 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
542 if (first_subtitle_language) {
543 lang = *first_subtitle_language;
545 for (auto const& i: _additional_subtitle_languages) {
551 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
554 auto metadata_list = meta->add_child("ExtensionMetadataList", "meta");
556 auto add_extension_metadata = [metadata_list](string scope, string name, string property_name, string property_value) {
557 auto extension = metadata_list->add_child("ExtensionMetadata", "meta");
558 extension->set_attribute("scope", scope);
559 extension->add_child("Name", "meta")->add_child_text(name);
560 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
561 property->add_child("Name", "meta")->add_child_text(property_name);
562 property->add_child("Value", "meta")->add_child_text(property_value);
565 /* SMPTE Bv2.1 8.6.3 */
566 add_extension_metadata ("http://isdcf.com/ns/cplmd/app", "Application", "DCP Constraints Profile", "SMPTE-RDD-52:2020-Bv2.1");
568 if (_sign_language_video_language) {
569 add_extension_metadata ("http://isdcf.com/2017/10/SignLanguageVideo", "Sign Language Video", "Language Tag", *_sign_language_video_language);
572 if (_dolby_edr_image_transfer_function) {
573 add_extension_metadata("http://www.dolby.com/schemas/2014/EDR-Metadata", "Dolby EDR", "image transfer function", *_dolby_edr_image_transfer_function);
576 if (_reels.front()->main_sound()) {
577 auto asset = _reels.front()->main_sound()->asset();
578 if (asset && include_mca_subdescriptors) {
579 write_mca_subdescriptors(meta, asset);
587 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
589 for (auto i: reels) {
590 if (i->main_picture ()) {
591 assets.push_back (i->main_picture());
593 if (i->main_sound ()) {
594 assets.push_back (i->main_sound());
596 if (i->main_subtitle ()) {
597 assets.push_back (i->main_subtitle());
599 for (auto j: i->closed_captions()) {
600 assets.push_back (j);
603 assets.push_back (i->atmos());
609 vector<shared_ptr<ReelFileAsset>>
610 CPL::reel_file_assets ()
612 vector<shared_ptr<ReelFileAsset>> c;
613 add_file_assets (c, _reels);
618 vector<shared_ptr<const ReelFileAsset>>
619 CPL::reel_file_assets () const
621 vector<shared_ptr<const ReelFileAsset>> c;
622 add_file_assets (c, _reels);
628 CPL::equals(shared_ptr<const Asset> other, EqualityOptions const& opt, NoteHandler note) const
630 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
635 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
636 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
637 note (NoteType::ERROR, s);
641 if (_content_kind != other_cpl->_content_kind) {
642 note (NoteType::ERROR, "CPL: content kinds differ");
646 if (_reels.size() != other_cpl->_reels.size()) {
647 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
651 auto a = _reels.begin();
652 auto b = other_cpl->_reels.begin();
654 while (a != _reels.end ()) {
655 if (!(*a)->equals (*b, opt, note)) {
667 CPL::any_encrypted () const
669 for (auto i: _reels) {
670 if (i->any_encrypted()) {
680 CPL::all_encrypted () const
682 for (auto i: _reels) {
683 if (!i->all_encrypted()) {
693 CPL::add (DecryptedKDM const & kdm)
695 for (auto i: _reels) {
701 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
703 for (auto i: _reels) {
704 i->resolve_refs (assets);
709 CPL::pkl_type (Standard standard) const
711 return static_pkl_type (standard);
715 CPL::static_pkl_type (Standard standard)
718 case Standard::INTEROP:
719 return "text/xml;asdcpKind=CPL";
720 case Standard::SMPTE:
728 CPL::duration () const
731 for (auto i: _reels) {
739 CPL::set_version_number (int v)
742 throw BadSettingError ("CPL version number cannot be negative");
750 CPL::unset_version_number ()
752 _version_number = boost::none;
757 CPL::set_content_versions (vector<ContentVersion> v)
759 std::set<string> ids;
761 if (!ids.insert(i.id).second) {
762 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
766 _content_versions = v;
770 optional<ContentVersion>
771 CPL::content_version () const
773 if (_content_versions.empty()) {
774 return optional<ContentVersion>();
777 return _content_versions[0];
782 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
784 _additional_subtitle_languages.clear ();
785 for (auto const& i: langs) {
786 _additional_subtitle_languages.push_back (i.to_string());
792 CPL::set_main_picture_active_area(dcp::Size area)
794 if (area.width % 2) {
795 throw BadSettingError("Main picture active area width is not a multiple of 2");
798 if (area.height % 2) {
799 throw BadSettingError("Main picture active area height is not a multiple of 2");
802 _main_picture_active_area = area;