Merge master.
authorCarl Hetherington <cth@carlh.net>
Sun, 7 Sep 2014 22:36:40 +0000 (23:36 +0100)
committerCarl Hetherington <cth@carlh.net>
Sun, 7 Sep 2014 22:36:40 +0000 (23:36 +0100)
1  2 
ChangeLog
src/lib/config.cc
src/lib/config.h
src/lib/video_content.cc
src/lib/video_content.h
src/tools/dcpomatic.cc
src/wx/config_dialog.cc

diff --combined ChangeLog
index ca321f9720eb0e0055b13ff470d1be2832051a10,99cc20dc12472051a8be8655867a758b054f52e9..d8210ef9d992dc21feecc26cc60c2e63a347e9d6
+++ b/ChangeLog
@@@ -1,3 -1,10 +1,10 @@@
+ 2014-09-07  Carl Hetherington  <cth@carlh.net>
+       * Put no stretch / no scale in the set of choices for default
+       scale to.
+       * Fix a few bad fuzzy translations from the preferences dialog.
  2014-09-03  Carl Hetherington  <cth@carlh.net>
  
        * Version 1.73.2 released.
  
  2014-08-29  Carl Hetherington  <cth@carlh.net>
  
 +      * Version 2.0.4 released.
 +
 +2014-08-24  Carl Hetherington  <cth@carlh.net>
 +
 +      * Version 2.0.3 released.
 +
 +2014-08-24  Carl Hetherington  <cth@carlh.net>
 +
 +      * Version 2.0.2 released.
 +
 +2014-08-06  Carl Hetherington  <cth@carlh.net>
 +
 +      * Version 2.0.1 released.
 +
 +2014-07-15  Carl Hetherington  <cth@carlh.net>
 +
 +      * A variety of changes were made on the 2.0 branch
 +      but not documented in the ChangeLog.  Most sigificantly:
 +
 +      - DCP import
 +      - Creation of DCPs with proper XML subtitles
 +      - Import of .srt and .xml subtitles
 +      - Audio processing framework (with some basic processors).
 +
 +2014-03-07  Carl Hetherington  <cth@carlh.net>
 +
 +      * Add subtitle view.
        * Some improvements to the manual.
  
  2014-08-26  Carl Hetherington  <cth@carlh.net>
@@@ -87,7 -67,6 +94,7 @@@
        * Attempt to fix random crashes on OS X (especially during encodes)
        thought to be caused by multiple threads using (different) stringstreams
        at the same time; see src/lib/safe_stringstream.
 +>>>>>>> origin/master
  
  2014-08-09  Carl Hetherington  <cth@carlh.net>
  
  2014-07-10  Carl Hetherington  <cth@carlh.net>
  
        * Version 1.72.2 released.
 +>>>>>>> origin/master
  
  2014-07-10  Carl Hetherington  <cth@carlh.net>
  
diff --combined src/lib/config.cc
index 1f5a25ae4dcb52f785930d8948bb5998f25dbb23,878fedaa4c31ed394bad0c43819ea7f961c1fdcb..114fc5c274c7753cdc0f2e0f721603ecaf47cd12
  #include <glib.h>
  #include <boost/filesystem.hpp>
  #include <boost/algorithm/string.hpp>
 -#include <libdcp/colour_matrix.h>
 -#include <libdcp/raw_convert.h>
 +#include <dcp/colour_matrix.h>
 +#include <dcp/raw_convert.h>
 +#include <dcp/signer.h>
 +#include <dcp/certificate_chain.h>
  #include <libcxml/cxml.h>
  #include "config.h"
  #include "server.h"
  #include "filter.h"
  #include "ratio.h"
  #include "dcp_content_type.h"
 -#include "sound_processor.h"
 +#include "cinema_sound_processor.h"
  #include "colour_conversion.h"
  #include "cinema.h"
  #include "util.h"
 +#include "cross.h"
  
  #include "i18n.h"
  
@@@ -54,7 -51,7 +54,7 @@@ using boost::shared_ptr
  using boost::optional;
  using boost::algorithm::is_any_of;
  using boost::algorithm::split;
 -using libdcp::raw_convert;
 +using dcp::raw_convert;
  
  Config* Config::_instance = 0;
  
@@@ -64,10 -61,10 +64,10 @@@ Config::Config (
        , _server_port_base (6192)
        , _use_any_servers (true)
        , _tms_path (".")
 -      , _sound_processor (SoundProcessor::from_id (N_("dolby_cp750")))
 +      , _cinema_sound_processor (CinemaSoundProcessor::from_id (N_("dolby_cp750")))
        , _allow_any_dcp_frame_rate (false)
        , _default_still_length (10)
-       , _default_scale (Ratio::from_id ("185"))
+       , _default_scale (VideoContentScale (Ratio::from_id ("185")))
        , _default_container (Ratio::from_id ("185"))
        , _default_dcp_content_type (DCPContentType::from_isdcf_name ("TST"))
        , _default_j2k_bandwidth (100000000)
@@@ -84,9 -81,9 +84,9 @@@
        _allowed_dcp_frame_rates.push_back (50);
        _allowed_dcp_frame_rates.push_back (60);
  
 -      _colour_conversions.push_back (PresetColourConversion (_("sRGB"), 2.4, true, libdcp::colour_matrix::srgb_to_xyz, 2.6));
 -      _colour_conversions.push_back (PresetColourConversion (_("sRGB non-linearised"), 2.4, false, libdcp::colour_matrix::srgb_to_xyz, 2.6));
 -      _colour_conversions.push_back (PresetColourConversion (_("Rec. 709"), 2.2, false, libdcp::colour_matrix::rec709_to_xyz, 2.6));
 +      _colour_conversions.push_back (PresetColourConversion (_("sRGB"), 2.4, true, dcp::colour_matrix::srgb_to_xyz, 2.6));
 +      _colour_conversions.push_back (PresetColourConversion (_("sRGB non-linearised"), 2.4, false, dcp::colour_matrix::srgb_to_xyz, 2.6));
 +      _colour_conversions.push_back (PresetColourConversion (_("Rec. 709"), 2.2, false, dcp::colour_matrix::rec709_to_xyz, 2.6));
  
        reset_kdm_email ();
  }
@@@ -95,10 -92,7 +95,10 @@@ voi
  Config::read ()
  {
        if (!boost::filesystem::exists (file (false))) {
 -              read_old_metadata ();
 +              /* Make a new set of signing certificates and key */
 +              _signer.reset (new dcp::Signer (openssl_path ()));
 +              /* And decryption keys */
 +              make_decryption_keys ();
                return;
        }
  
  
        c = f.optional_string_child ("SoundProcessor");
        if (c) {
 -              _sound_processor = SoundProcessor::from_id (c.get ());
 +              _cinema_sound_processor = CinemaSoundProcessor::from_id (c.get ());
 +      }
 +      c = f.optional_string_child ("CinemaSoundProcessor");
 +      if (c) {
 +              _cinema_sound_processor = CinemaSoundProcessor::from_id (c.get ());
        }
  
        _language = f.optional_string_child ("Language");
  
        c = f.optional_string_child ("DefaultScale");
        if (c) {
-               _default_scale = Ratio::from_id (c.get ());
+               _default_scale = VideoContentScale::from_id (c.get ());
        }
  
        c = f.optional_string_child ("DefaultContainer");
                /* Loading version 0 (before Rec. 709 was added as a preset).
                   Add it in.
                */
 -              _colour_conversions.push_back (PresetColourConversion (_("Rec. 709"), 2.2, false, libdcp::colour_matrix::rec709_to_xyz, 2.6));
 +              _colour_conversions.push_back (PresetColourConversion (_("Rec. 709"), 2.2, false, dcp::colour_matrix::rec709_to_xyz, 2.6));
        }
  
        list<cxml::NodePtr> cin = f.node_children ("Cinema");
        for (list<cxml::NodePtr>::const_iterator i = his.begin(); i != his.end(); ++i) {
                _history.push_back ((*i)->content ());
        }
 -}
  
 -void
 -Config::read_old_metadata ()
 -{
 -      /* XXX: this won't work with non-Latin filenames */
 -      ifstream f (file(true).string().c_str ());
 -      string line;
 -
 -      while (getline (f, line)) {
 -              if (line.empty ()) {
 -                      continue;
 +      cxml::NodePtr signer = f.optional_node_child ("Signer");
 +      dcp::CertificateChain signer_chain;
 +      if (signer) {
 +              /* Read the signing certificates and private key in from the config file */
 +              list<cxml::NodePtr> certificates = signer->node_children ("Certificate");
 +              for (list<cxml::NodePtr>::const_iterator i = certificates.begin(); i != certificates.end(); ++i) {
 +                      signer_chain.add (dcp::Certificate ((*i)->content ()));
                }
  
 -              if (line[0] == '#') {
 -                      continue;
 -              }
 +              _signer.reset (new dcp::Signer (signer_chain, signer->string_child ("PrivateKey")));
 +      } else {
 +              /* Make a new set of signing certificates and key */
 +              _signer.reset (new dcp::Signer (openssl_path ()));
 +      }
  
 -              size_t const s = line.find (' ');
 -              if (s == string::npos) {
 -                      continue;
 -              }
 -              
 -              string const k = line.substr (0, s);
 -              string const v = line.substr (s + 1);
 -
 -              if (k == N_("num_local_encoding_threads")) {
 -                      _num_local_encoding_threads = atoi (v.c_str ());
 -              } else if (k == N_("default_directory")) {
 -                      _default_directory = v;
 -              } else if (k == N_("server_port")) {
 -                      _server_port_base = atoi (v.c_str ());
 -              } else if (k == N_("server")) {
 -                      vector<string> b;
 -                      split (b, v, is_any_of (" "));
 -                      if (b.size() == 2) {
 -                              _servers.push_back (b[0]);
 -                      }
 -              } else if (k == N_("tms_ip")) {
 -                      _tms_ip = v;
 -              } else if (k == N_("tms_path")) {
 -                      _tms_path = v;
 -              } else if (k == N_("tms_user")) {
 -                      _tms_user = v;
 -              } else if (k == N_("tms_password")) {
 -                      _tms_password = v;
 -              } else if (k == N_("sound_processor")) {
 -                      _sound_processor = SoundProcessor::from_id (v);
 -              } else if (k == "language") {
 -                      _language = v;
 -              } else if (k == "default_container") {
 -                      _default_container = Ratio::from_id (v);
 -              } else if (k == "default_dcp_content_type") {
 -                      _default_dcp_content_type = DCPContentType::from_isdcf_name (v);
 -              } else if (k == "dcp_metadata_issuer") {
 -                      _dcp_issuer = v;
 -              }
 +      if (f.optional_string_child ("DecryptionCertificate")) {
 +              _decryption_certificate = dcp::Certificate (f.string_child ("DecryptionCertificate"));
 +      }
 +
 +      if (f.optional_string_child ("DecryptionPrivateKey")) {
 +              _decryption_private_key = f.string_child ("DecryptionPrivateKey");
 +      }
  
 -              _default_isdcf_metadata.read_old_metadata (k, v);
 +      if (!f.optional_string_child ("DecryptionCertificate") || !f.optional_string_child ("DecryptionPrivateKey")) {
 +              /* Generate our own decryption certificate and key if either is not present in config */
 +              make_decryption_keys ();
        }
  }
  
 +void
 +Config::make_decryption_keys ()
 +{
 +      boost::filesystem::path p = dcp::make_certificate_chain (openssl_path ());
 +      _decryption_certificate = dcp::Certificate (dcp::file_to_string (p / "leaf.signed.pem"));
 +      _decryption_private_key = dcp::file_to_string (p / "leaf.key");
 +      boost::filesystem::remove_all (p);
 +}
 +
  /** @return Filename to write configuration to */
  boost::filesystem::path
  Config::file (bool old) const
        return p;
  }
  
 -boost::filesystem::path
 -Config::signer_chain_directory () const
 -{
 -      boost::filesystem::path p;
 -      p /= g_get_user_config_dir ();
 -      p /= "dcpomatic";
 -      p /= "crypt";
 -      boost::filesystem::create_directories (p);
 -      return p;
 -}
 -
  /** @return Singleton instance */
  Config *
  Config::instance ()
@@@ -323,15 -347,13 +323,13 @@@ Config::write () cons
        root->add_child("TMSPath")->add_child_text (_tms_path);
        root->add_child("TMSUser")->add_child_text (_tms_user);
        root->add_child("TMSPassword")->add_child_text (_tms_password);
 -      if (_sound_processor) {
 -              root->add_child("SoundProcessor")->add_child_text (_sound_processor->id ());
 +      if (_cinema_sound_processor) {
 +              root->add_child("CinemaSoundProcessor")->add_child_text (_cinema_sound_processor->id ());
        }
        if (_language) {
                root->add_child("Language")->add_child_text (_language.get());
        }
-       if (_default_scale) {
-               root->add_child("DefaultScale")->add_child_text (_default_scale->id ());
-       }
+       root->add_child("DefaultScale")->add_child_text (_default_scale.id ());
        if (_default_container) {
                root->add_child("DefaultContainer")->add_child_text (_default_container->id ());
        }
        root->add_child("AllowAnyDCPFrameRate")->add_child_text (_allow_any_dcp_frame_rate ? "1" : "0");
        root->add_child("LogTypes")->add_child_text (raw_convert<string> (_log_types));
  
 +      xmlpp::Element* signer = root->add_child ("Signer");
 +      dcp::CertificateChain::List certs = _signer->certificates().root_to_leaf ();
 +      for (dcp::CertificateChain::List::const_iterator i = certs.begin(); i != certs.end(); ++i) {
 +              signer->add_child("Certificate")->add_child_text (i->certificate (true));
 +      }
 +      signer->add_child("PrivateKey")->add_child_text (_signer->key ());
 +
 +      root->add_child("DecryptionCertificate")->add_child_text (_decryption_certificate.certificate (true));
 +      root->add_child("DecryptionPrivateKey")->add_child_text (_decryption_private_key);
 +
        for (vector<boost::filesystem::path>::const_iterator i = _history.begin(); i != _history.end(); ++i) {
                root->add_child("History")->add_child_text (i->string ());
        }
diff --combined src/lib/config.h
index 9a18086829af77194d973941399b93ee80c1a52a,0639382a05040abc85e7e4fc1a9075a591dd974c..99b3eb62122e00529502db5100f5e069a35c4c3e
  #include <boost/shared_ptr.hpp>
  #include <boost/signals2.hpp>
  #include <boost/filesystem.hpp>
 +#include <dcp/metadata.h>
 +#include <dcp/certificates.h>
 +#include <dcp/signer.h>
  #include "isdcf_metadata.h"
  #include "colour_conversion.h"
 -#include "server.h"
+ #include "video_content.h"
  
  class ServerDescription;
  class Scaler;
  class Filter;
 -class SoundProcessor;
 +class CinemaSoundProcessor;
  class DCPContentType;
  class Ratio;
  class Cinema;
@@@ -105,9 -104,9 +106,9 @@@ public
                return _tms_password;
        }
  
 -      /** @return The sound processor that we are using */
 -      SoundProcessor const * sound_processor () const {
 -              return _sound_processor;
 +      /** @return The cinema sound processor that we are using */
 +      CinemaSoundProcessor const * cinema_sound_processor () const {
 +              return _cinema_sound_processor;
        }
  
        std::list<boost::shared_ptr<Cinema> > cinemas () const {
                return _default_still_length;
        }
  
-       Ratio const * default_scale () const {
+       VideoContentScale default_scale () const {
                return _default_scale;
        }
  
                return _kdm_email;
        }
  
 +      boost::shared_ptr<const dcp::Signer> signer () const {
 +              return _signer;
 +      }
 +
 +      dcp::Certificate decryption_certificate () const {
 +              return _decryption_certificate;
 +      }
 +
 +      std::string decryption_private_key () const {
 +              return _decryption_private_key;
 +      }
 +
        bool check_for_updates () const {
                return _check_for_updates;
        }
                changed ();
        }
  
-       void set_default_scale (Ratio const * s) {
+       void set_default_scale (VideoContentScale s) {
                _default_scale = s;
                changed ();
        }
  
        void reset_kdm_email ();
  
 +      void set_signer (boost::shared_ptr<const dcp::Signer> s) {
 +              _signer = s;
 +              changed ();
 +      }
 +
 +      void set_decryption_certificate (dcp::Certificate c) {
 +              _decryption_certificate = c;
 +              changed ();
 +      }
 +
 +      void set_decryption_private_key (std::string k) {
 +              _decryption_private_key = k;
 +              changed ();
 +      }
 +
        void set_check_for_updates (bool c) {
                _check_for_updates = c;
                changed ();
  
        void add_to_history (boost::filesystem::path p);
        
 -      boost::filesystem::path signer_chain_directory () const;
 -
        void changed ();
        boost::signals2::signal<void ()> Changed;
  
@@@ -436,8 -410,8 +437,8 @@@ private
        Config ();
        boost::filesystem::path file (bool) const;
        void read ();
 -      void read_old_metadata ();
        void write () const;
 +      void make_decryption_keys ();
  
        /** number of threads to use for J2K encoding on the local machine */
        int _num_local_encoding_threads;
        std::string _tms_user;
        /** Password to log into the TMS with */
        std::string _tms_password;
 -      /** Our sound processor */
 -      SoundProcessor const * _sound_processor;
 +      /** Our cinema sound processor */
 +      CinemaSoundProcessor const * _cinema_sound_processor;
        std::list<int> _allowed_dcp_frame_rates;
        /** Allow any video frame rate for the DCP; if true, overrides _allowed_dcp_frame_rates */
        bool _allow_any_dcp_frame_rate;
        ISDCFMetadata _default_isdcf_metadata;
        boost::optional<std::string> _language;
        int _default_still_length;
-       Ratio const * _default_scale;
+       VideoContentScale _default_scale;
        Ratio const * _default_container;
        DCPContentType const * _default_dcp_content_type;
        std::string _dcp_issuer;
        std::string _kdm_cc;
        std::string _kdm_bcc;
        std::string _kdm_email;
 +      boost::shared_ptr<const dcp::Signer> _signer;
 +      dcp::Certificate _decryption_certificate;
 +      std::string _decryption_private_key;
        /** true to check for updates on startup */
        bool _check_for_updates;
        bool _check_for_test_updates;
diff --combined src/lib/video_content.cc
index 796e6575a2e62f2088affaafea41a7240adb866b,5a80dba7455cab235aaa34f1d7855b681a9f4620..9822d77634c659a583de5314a4904032da104ec4
@@@ -19,8 -19,8 +19,8 @@@
  
  #include <iomanip>
  #include <libcxml/cxml.h>
 -#include <libdcp/colour_matrix.h>
 -#include <libdcp/raw_convert.h>
 +#include <dcp/colour_matrix.h>
 +#include <dcp/raw_convert.h>
  #include "video_content.h"
  #include "video_examiner.h"
  #include "compose.hpp"
  #include "film.h"
  #include "exceptions.h"
  #include "frame_rate_change.h"
 +#include "log.h"
  #include "safe_stringstream.h"
  
  #include "i18n.h"
  
 +#define LOG_GENERAL(...) film->log()->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL);
 +
  int const VideoContentProperty::VIDEO_SIZE      = 0;
  int const VideoContentProperty::VIDEO_FRAME_RATE  = 1;
  int const VideoContentProperty::VIDEO_FRAME_TYPE  = 2;
@@@ -54,13 -51,14 +54,13 @@@ using std::max
  using boost::shared_ptr;
  using boost::optional;
  using boost::dynamic_pointer_cast;
 -using libdcp::raw_convert;
 +using dcp::raw_convert;
  
  vector<VideoContentScale> VideoContentScale::_scales;
  
  VideoContent::VideoContent (shared_ptr<const Film> f)
        : Content (f)
        , _video_length (0)
 -      , _original_video_frame_rate (0)
        , _video_frame_rate (0)
        , _video_frame_type (VIDEO_FRAME_TYPE_2D)
        , _scale (Config::instance()->default_scale ())
        setup_default_colour_conversion ();
  }
  
 -VideoContent::VideoContent (shared_ptr<const Film> f, Time s, VideoContent::Frame len)
 +VideoContent::VideoContent (shared_ptr<const Film> f, DCPTime s, ContentTime len)
        : Content (f, s)
        , _video_length (len)
 -      , _original_video_frame_rate (0)
        , _video_frame_rate (0)
        , _video_frame_type (VIDEO_FRAME_TYPE_2D)
        , _scale (Config::instance()->default_scale ())
@@@ -81,6 -80,7 +81,6 @@@
  VideoContent::VideoContent (shared_ptr<const Film> f, boost::filesystem::path p)
        : Content (f, p)
        , _video_length (0)
 -      , _original_video_frame_rate (0)
        , _video_frame_rate (0)
        , _video_frame_type (VIDEO_FRAME_TYPE_2D)
        , _scale (Config::instance()->default_scale ())
        setup_default_colour_conversion ();
  }
  
 -VideoContent::VideoContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node, int version)
 +VideoContent::VideoContent (shared_ptr<const Film> f, cxml::ConstNodePtr node, int version)
        : Content (f, node)
  {
 -      _video_length = node->number_child<VideoContent::Frame> ("VideoLength");
        _video_size.width = node->number_child<int> ("VideoWidth");
        _video_size.height = node->number_child<int> ("VideoHeight");
        _video_frame_rate = node->number_child<float> ("VideoFrameRate");
 -      _original_video_frame_rate = node->optional_number_child<float> ("OriginalVideoFrameRate").get_value_or (_video_frame_rate);
 +
 +      if (version < 32) {
 +              /* DCP-o-matic 1.0 branch */
 +              _video_length = ContentTime::from_frames (node->number_child<int64_t> ("VideoLength"), _video_frame_rate);
 +      } else {
 +              _video_length = ContentTime (node->number_child<ContentTime::Type> ("VideoLength"));
 +      }
 +      
        _video_frame_type = static_cast<VideoFrameType> (node->number_child<int> ("VideoFrameType"));
        _crop.left = node->number_child<int> ("LeftCrop");
        _crop.right = node->number_child<int> ("RightCrop");
@@@ -158,6 -152,7 +158,6 @@@ VideoContent::VideoContent (shared_ptr<
        }
  
        _video_size = ref->video_size ();
 -      _original_video_frame_rate = ref->original_video_frame_rate ();
        _video_frame_rate = ref->video_frame_rate ();
        _video_frame_type = ref->video_frame_type ();
        _crop = ref->crop ();
@@@ -169,10 -164,11 +169,10 @@@ voi
  VideoContent::as_xml (xmlpp::Node* node) const
  {
        boost::mutex::scoped_lock lm (_mutex);
 -      node->add_child("VideoLength")->add_child_text (raw_convert<string> (_video_length));
 +      node->add_child("VideoLength")->add_child_text (raw_convert<string> (_video_length.get ()));
        node->add_child("VideoWidth")->add_child_text (raw_convert<string> (_video_size.width));
        node->add_child("VideoHeight")->add_child_text (raw_convert<string> (_video_size.height));
        node->add_child("VideoFrameRate")->add_child_text (raw_convert<string> (_video_frame_rate));
 -      node->add_child("OriginalVideoFrameRate")->add_child_text (raw_convert<string> (_original_video_frame_rate));
        node->add_child("VideoFrameType")->add_child_text (raw_convert<string> (static_cast<int> (_video_frame_type)));
        _crop.as_xml (node);
        _scale.as_xml (node->add_child("Scale"));
  void
  VideoContent::setup_default_colour_conversion ()
  {
 -      _colour_conversion = PresetColourConversion (_("sRGB"), 2.4, true, libdcp::colour_matrix::srgb_to_xyz, 2.6).conversion;
 +      _colour_conversion = PresetColourConversion (_("sRGB"), 2.4, true, dcp::colour_matrix::srgb_to_xyz, 2.6).conversion;
  }
  
  void
  VideoContent::take_from_video_examiner (shared_ptr<VideoExaminer> d)
  {
        /* These examiner calls could call other content methods which take a lock on the mutex */
 -      libdcp::Size const vs = d->video_size ();
 +      dcp::Size const vs = d->video_size ();
        float const vfr = d->video_frame_rate ();
 -      
 +      ContentTime vl = d->video_length ();
 +
        {
                boost::mutex::scoped_lock lm (_mutex);
                _video_size = vs;
                _video_frame_rate = vfr;
 -              _original_video_frame_rate = vfr;
 +              _video_length = vl;
        }
 +
 +      shared_ptr<const Film> film = _film.lock ();
 +      assert (film);
 +      LOG_GENERAL ("Video length obtained from header as %1 frames", _video_length.frames (_video_frame_rate));
        
        signal_changed (VideoContentProperty::VIDEO_SIZE);
        signal_changed (VideoContentProperty::VIDEO_FRAME_RATE);
 +      signal_changed (ContentProperty::LENGTH);
  }
  
  
@@@ -337,17 -327,14 +337,17 @@@ VideoContent::technical_summary () cons
  {
        return String::compose (
                "video: length %1, size %2x%3, rate %4",
 -              video_length_after_3d_combine(), video_size().width, video_size().height, video_frame_rate()
 +              video_length_after_3d_combine().seconds(),
 +              video_size().width,
 +              video_size().height,
 +              video_frame_rate()
                );
  }
  
 -libdcp::Size
 +dcp::Size
  VideoContent::video_size_after_3d_split () const
  {
 -      libdcp::Size const s = video_size ();
 +      dcp::Size const s = video_size ();
        switch (video_frame_type ()) {
        case VIDEO_FRAME_TYPE_2D:
        case VIDEO_FRAME_TYPE_3D_ALTERNATE:
        case VIDEO_FRAME_TYPE_3D_RIGHT:
                return s;
        case VIDEO_FRAME_TYPE_3D_LEFT_RIGHT:
 -              return libdcp::Size (s.width / 2, s.height);
 +              return dcp::Size (s.width / 2, s.height);
        case VIDEO_FRAME_TYPE_3D_TOP_BOTTOM:
 -              return libdcp::Size (s.width, s.height / 2);
 +              return dcp::Size (s.width, s.height / 2);
        }
  
        assert (false);
@@@ -375,21 -362,28 +375,21 @@@ VideoContent::set_colour_conversion (Co
  }
  
  /** @return Video size after 3D split and crop */
 -libdcp::Size
 +dcp::Size
  VideoContent::video_size_after_crop () const
  {
        return crop().apply (video_size_after_3d_split ());
  }
  
  /** @param t A time offset from the start of this piece of content.
 - *  @return Corresponding frame index.
 + *  @return Corresponding time with respect to the content.
   */
 -VideoContent::Frame
 -VideoContent::time_to_content_video_frames (Time t) const
 +ContentTime
 +VideoContent::dcp_time_to_content_time (DCPTime t) const
  {
        shared_ptr<const Film> film = _film.lock ();
        assert (film);
 -      
 -      FrameRateChange frc (video_frame_rate(), film->video_frame_rate());
 -
 -      /* Here we are converting from time (in the DCP) to a frame number in the content.
 -         Hence we need to use the DCP's frame rate and the double/skip correction, not
 -         the source's rate.
 -      */
 -      return t * film->video_frame_rate() / (frc.factor() * TIME_HZ);
 +      return ContentTime (t, FrameRateChange (video_frame_rate(), film->video_frame_rate()));
  }
  
  void
@@@ -454,7 -448,7 +454,7 @@@ VideoContentScale::VideoContentScale (b
  
  }
  
 -VideoContentScale::VideoContentScale (shared_ptr<cxml::Node> node)
 +VideoContentScale::VideoContentScale (cxml::NodePtr node)
        : _ratio (0)
        , _scale (true)
  {
@@@ -482,7 -476,7 +482,7 @@@ VideoContentScale::id () cons
        SafeStringStream s;
        
        if (_ratio) {
-               s << _ratio->id () << "_";
+               s << _ratio->id ();
        } else {
                s << (_scale ? "S1" : "S0");
        }
@@@ -504,29 -498,44 +504,44 @@@ VideoContentScale::name () cons
        return _("No scale");
  }
  
+ VideoContentScale
+ VideoContentScale::from_id (string id)
+ {
+       Ratio const * r = Ratio::from_id (id);
+       if (r) {
+               return VideoContentScale (r);
+       }
+       if (id == "S0") {
+               return VideoContentScale (false);
+       }
+       return VideoContentScale (true);
+ }
+               
  /** @param display_container Size of the container that we are displaying this content in.
   *  @param film_container The size of the film's image.
   */
 -libdcp::Size
 -VideoContentScale::size (shared_ptr<const VideoContent> c, libdcp::Size display_container, libdcp::Size film_container) const
 +dcp::Size
 +VideoContentScale::size (shared_ptr<const VideoContent> c, dcp::Size display_container, dcp::Size film_container, int round) const
  {
        if (_ratio) {
 -              return fit_ratio_within (_ratio->ratio (), display_container);
 +              return fit_ratio_within (_ratio->ratio (), display_container, round);
        }
  
 -      libdcp::Size const ac = c->video_size_after_crop ();
 +      dcp::Size const ac = c->video_size_after_crop ();
  
        /* Force scale if the film_container is smaller than the content's image */
        if (_scale || film_container.width < ac.width || film_container.height < ac.height) {
 -              return fit_ratio_within (ac.ratio (), display_container);
 +              return fit_ratio_within (ac.ratio (), display_container, 1);
        }
  
        /* Scale the image so that it will be in the right place in film_container, even if display_container is a
           different size.
        */
 -      return libdcp::Size (
 -              c->video_size().width  * float(display_container.width)  / film_container.width,
 -              c->video_size().height * float(display_container.height) / film_container.height
 +      return dcp::Size (
 +              round_to (c->video_size().width  * float(display_container.width)  / film_container.width, round),
 +              round_to (c->video_size().height * float(display_container.height) / film_container.height, round)
                );
  }
  
diff --combined src/lib/video_content.h
index 27b36e9bc65ed73358ae36ef2da2adb32c607f77,9404676808504d912814e70673d9d28e348826bb..d32769b5a9dde2ec41128207983e04b9721b8c0e
@@@ -43,9 -43,9 +43,9 @@@ public
        VideoContentScale ();
        VideoContentScale (Ratio const *);
        VideoContentScale (bool);
 -      VideoContentScale (boost::shared_ptr<cxml::Node>);
 +      VideoContentScale (cxml::NodePtr);
  
 -      libdcp::Size size (boost::shared_ptr<const VideoContent>, libdcp::Size, libdcp::Size) const;
 +      dcp::Size size (boost::shared_ptr<const VideoContent>, dcp::Size, dcp::Size, int round) const;
        std::string id () const;
        std::string name () const;
        void as_xml (xmlpp::Node *) const;
@@@ -62,6 -62,7 +62,7 @@@
        static std::vector<VideoContentScale> all () {
                return _scales;
        }
+       static VideoContentScale from_id (std::string id);
  
  private:
        /** a ratio to stretch the content to, or 0 for no stretch */
@@@ -81,9 -82,9 +82,9 @@@ public
        typedef int Frame;
  
        VideoContent (boost::shared_ptr<const Film>);
 -      VideoContent (boost::shared_ptr<const Film>, Time, VideoContent::Frame);
 +      VideoContent (boost::shared_ptr<const Film>, DCPTime, ContentTime);
        VideoContent (boost::shared_ptr<const Film>, boost::filesystem::path);
 -      VideoContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>, int);
 +      VideoContent (boost::shared_ptr<const Film>, cxml::ConstNodePtr, int);
        VideoContent (boost::shared_ptr<const Film>, std::vector<boost::shared_ptr<Content> >);
  
        void as_xml (xmlpp::Node *) const;
        virtual std::string information () const;
        virtual std::string identifier () const;
  
 -      VideoContent::Frame video_length () const {
 +      ContentTime video_length () const {
                boost::mutex::scoped_lock lm (_mutex);
                return _video_length;
        }
  
 -      VideoContent::Frame video_length_after_3d_combine () const {
 +      ContentTime video_length_after_3d_combine () const {
                boost::mutex::scoped_lock lm (_mutex);
                if (_video_frame_type == VIDEO_FRAME_TYPE_3D_ALTERNATE) {
 -                      return _video_length / 2;
 +                      return ContentTime (_video_length.get() / 2);
                }
                
                return _video_length;
        }
  
 -      libdcp::Size video_size () const {
 +      dcp::Size video_size () const {
                boost::mutex::scoped_lock lm (_mutex);
                return _video_size;
        }
                return _video_frame_rate;
        }
  
 -      float original_video_frame_rate () const {
 -              boost::mutex::scoped_lock lm (_mutex);
 -              return _original_video_frame_rate;
 -      }
 -      
        void set_video_frame_type (VideoFrameType);
        void set_video_frame_rate (float);
  
                return _colour_conversion;
        }
  
 -      libdcp::Size video_size_after_3d_split () const;
 -      libdcp::Size video_size_after_crop () const;
 +      dcp::Size video_size_after_3d_split () const;
 +      dcp::Size video_size_after_crop () const;
  
 -      VideoContent::Frame time_to_content_video_frames (Time) const;
 +      ContentTime dcp_time_to_content_time (DCPTime) const;
  
        void scale_and_crop_to_fit_width ();
        void scale_and_crop_to_fit_height ();
  protected:
        void take_from_video_examiner (boost::shared_ptr<VideoExaminer>);
  
 -      VideoContent::Frame _video_length;
 -      float _original_video_frame_rate;
 +      ContentTime _video_length;
        float _video_frame_rate;
  
  private:
 -      friend class ffmpeg_pts_offset_test;
 -      friend class best_dcp_frame_rate_test_single;
 -      friend class best_dcp_frame_rate_test_double;
 -      friend class audio_sampling_rate_test;
 +      friend struct ffmpeg_pts_offset_test;
 +      friend struct best_dcp_frame_rate_test_single;
 +      friend struct best_dcp_frame_rate_test_double;
 +      friend struct audio_sampling_rate_test;
  
        void setup_default_colour_conversion ();
        
 -      libdcp::Size _video_size;
 +      dcp::Size _video_size;
        VideoFrameType _video_frame_type;
        Crop _crop;
        VideoContentScale _scale;
diff --combined src/tools/dcpomatic.cc
index 3bef7bce300f663be2ca9340f7e8d4f9313d6dab,c3dd8cd582231739f4b158bd63b24effa458ef66..5f6a980751c0806f8596bb0e8715884f0b2b3b3c
@@@ -30,7 -30,7 +30,7 @@@
  #include <wx/stdpaths.h>
  #include <wx/cmdline.h>
  #include <wx/preferences.h>
 -#include <libdcp/exceptions.h>
 +#include <dcp/exceptions.h>
  #include "wx/film_viewer.h"
  #include "wx/film_editor.h"
  #include "wx/job_manager_view.h"
@@@ -45,7 -45,6 +45,7 @@@
  #include "wx/servers_list_dialog.h"
  #include "wx/hints_dialog.h"
  #include "wx/update_dialog.h"
 +#include "wx/content_panel.h"
  #include "lib/film.h"
  #include "lib/config.h"
  #include "lib/util.h"
@@@ -164,7 -163,7 +164,7 @@@ public
                setup_menu (bar);
                SetMenuBar (bar);
  
-               Config::instance()->Changed.connect (boost::bind (&Frame::config_changed, this));
+               _config_changed_connection = Config::instance()->Changed.connect (boost::bind (&Frame::config_changed, this));
                config_changed ();
  
                Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::file_new, this),                ID_file_new);
@@@ -403,7 -402,7 +403,7 @@@ private
                                        shared_ptr<Job> (new SendKDMEmailJob (_film, d->screens (), d->cpl (), d->from (), d->until (), d->formulation ()))
                                        );
                        }
 -              } catch (libdcp::NotEncryptedError& e) {
 +              } catch (dcp::NotEncryptedError& e) {
                        error_dialog (this, _("CPL's content is not encrypted."));
                } catch (exception& e) {
                        error_dialog (this, e.what ());
  
        void content_scale_to_fit_width ()
        {
 -              VideoContentList vc = _film_editor->selected_video_content ();
 +              VideoContentList vc = _film_editor->content_panel()->selected_video ();
                for (VideoContentList::iterator i = vc.begin(); i != vc.end(); ++i) {
                        (*i)->scale_and_crop_to_fit_width ();
                }
  
        void content_scale_to_fit_height ()
        {
 -              VideoContentList vc = _film_editor->selected_video_content ();
 +              VideoContentList vc = _film_editor->content_panel()->selected_video ();
                for (VideoContentList::iterator i = vc.begin(); i != vc.end(); ++i) {
                        (*i)->scale_and_crop_to_fit_height ();
                }
                        return;
                }
  
+               /* We don't want to hear about any more configuration changes, since they
+                  cause the File menu to be altered, which itself will be deleted around
+                  now (without, as far as I can see, any way for us to find out).
+               */
+               _config_changed_connection.disconnect ();
+               
                maybe_save_then_delete_film ();
                ev.Skip ();
        }
  
                }
                bool const dcp_creation = (i != jobs.end ()) && !(*i)->finished ();
                bool const have_cpl = _film && !_film->cpls().empty ();
 -              bool const have_selected_video_content = !_film_editor->selected_video_content().empty();
 +              bool const have_selected_video_content = !_film_editor->content_panel()->selected_video().empty();
                
                for (map<wxMenuItem*, int>::iterator j = menu_items.begin(); j != menu_items.end(); ++j) {
                        
        int _history_items;
        int _history_position;
        wxMenuItem* _history_separator;
+       boost::signals2::scoped_connection _config_changed_connection;
  };
  
  static const wxCmdLineEntryDesc command_line_description[] = {
        { wxCMD_LINE_NONE, "", "", "", wxCmdLineParamType (0), 0 }
  };
  
 +/** @class App
 + *  @brief The magic App class for wxWidgets.
 + */
  class App : public wxApp
  {
        bool OnInit ()
diff --combined src/wx/config_dialog.cc
index 009467afa0fbfdbc7aceaf1eb0de191323a59be9,b18598d702c6dfff0f95eaffbe0ff942d365ff5c..c1ea926eafe669327f02e5bde3c153a81be723d3
  #include <wx/preferences.h>
  #include <wx/filepicker.h>
  #include <wx/spinctrl.h>
 -#include <libdcp/colour_matrix.h>
 +#include <dcp/colour_matrix.h>
 +#include <dcp/exceptions.h>
 +#include <dcp/signer.h>
  #include "lib/config.h"
  #include "lib/ratio.h"
  #include "lib/scaler.h"
  #include "lib/filter.h"
  #include "lib/dcp_content_type.h"
  #include "lib/colour_conversion.h"
 +#include "lib/log.h"
 +#include "lib/util.h"
 +#include "lib/cross.h"
 +#include "lib/exceptions.h"
  #include "config_dialog.h"
  #include "wx_util.h"
  #include "editable_list.h"
@@@ -112,6 -106,7 +112,6 @@@ public
                _num_local_encoding_threads = new wxSpinCtrl (panel);
                table->Add (_num_local_encoding_threads, 1);
  
 -              
                _check_for_updates = new wxCheckBox (panel, wxID_ANY, _("Check for updates on startup"));
                table->Add (_check_for_updates, 1, wxEXPAND | wxALL);
                table->AddSpacer (0);
@@@ -317,31 -312,31 +317,31 @@@ public
                
                _isdcf_metadata_button->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&DefaultsPage::edit_isdcf_metadata_clicked, this, parent));
                
-               vector<Ratio const *> ratio = Ratio::all ();
-               int n = 0;
-               for (vector<Ratio const *>::iterator i = ratio.begin(); i != ratio.end(); ++i) {
-                       _scale->Append (std_to_wx ((*i)->nickname ()));
-                       if (*i == config->default_scale ()) {
-                               _scale->SetSelection (n);
+               vector<VideoContentScale> scales = VideoContentScale::all ();
+               for (size_t i = 0; i < scales.size(); ++i) {
+                       _scale->Append (std_to_wx (scales[i].name ()));
+                       if (scales[i] == config->default_scale ()) {
+                               _scale->SetSelection (i);
                        }
-                       _container->Append (std_to_wx ((*i)->nickname ()));
-                       if (*i == config->default_container ()) {
-                               _container->SetSelection (n);
+               }
+               vector<Ratio const *> ratios = Ratio::all ();
+               for (size_t i = 0; i < ratios.size(); ++i) {
+                       _container->Append (std_to_wx (ratios[i]->nickname ()));
+                       if (ratios[i] == config->default_container ()) {
+                               _container->SetSelection (i);
                        }
-                       ++n;
                }
                
                _scale->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&DefaultsPage::scale_changed, this));
                _container->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&DefaultsPage::container_changed, this));
                
                vector<DCPContentType const *> const ct = DCPContentType::all ();
-               n = 0;
-               for (vector<DCPContentType const *>::const_iterator i = ct.begin(); i != ct.end(); ++i) {
-                       _dcp_content_type->Append (std_to_wx ((*i)->pretty_name ()));
-                       if (*i == config->default_dcp_content_type ()) {
-                               _dcp_content_type->SetSelection (n);
+               for (size_t i = 0; i < ct.size(); ++i) {
+                       _dcp_content_type->Append (std_to_wx (ct[i]->pretty_name ()));
+                       if (ct[i] == config->default_dcp_content_type ()) {
+                               _dcp_content_type->SetSelection (i);
                        }
-                       ++n;
                }
                
                _dcp_content_type->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&DefaultsPage::dcp_content_type_changed, this));
@@@ -391,8 -386,8 +391,8 @@@ private
  
        void scale_changed ()
        {
-               vector<Ratio const *> ratio = Ratio::all ();
-               Config::instance()->set_default_scale (ratio[_scale->GetSelection()]);
+               vector<VideoContentScale> scale = VideoContentScale::all ();
+               Config::instance()->set_default_scale (scale[_scale->GetSelection()]);
        }
        
        void container_changed ()
@@@ -535,310 -530,6 +535,310 @@@ private
        }
  };
  
 +class KeysPage : public wxPreferencesPage, public Page
 +{
 +public:
 +      KeysPage (wxSize panel_size, int border)
 +              : Page (panel_size, border)
 +      {}
 +
 +      wxString GetName () const
 +      {
 +              return _("Keys");
 +      }
 +
 +#ifdef DCPOMATIC_OSX
 +      wxBitmap GetLargeIcon () const
 +      {
 +              return wxBitmap ("keys", wxBITMAP_TYPE_PNG_RESOURCE);
 +      }
 +#endif        
 +
 +      wxWindow* CreateWindow (wxWindow* parent)
 +      {
 +              _panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, _panel_size);
 +              wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL);
 +              _panel->SetSizer (overall_sizer);
 +
 +              wxStaticText* m = new wxStaticText (_panel, wxID_ANY, _("Certificate chain for signing DCPs and KDMs:"));
 +              overall_sizer->Add (m, 0, wxALL, _border);
 +              
 +              wxBoxSizer* certificates_sizer = new wxBoxSizer (wxHORIZONTAL);
 +              overall_sizer->Add (certificates_sizer, 0, wxLEFT | wxRIGHT, _border);
 +              
 +              _certificates = new wxListCtrl (_panel, wxID_ANY, wxDefaultPosition, wxSize (400, 200), wxLC_REPORT | wxLC_SINGLE_SEL);
 +
 +              {
 +                      wxListItem ip;
 +                      ip.SetId (0);
 +                      ip.SetText (_("Type"));
 +                      ip.SetWidth (100);
 +                      _certificates->InsertColumn (0, ip);
 +              }
 +
 +              {
 +                      wxListItem ip;
 +                      ip.SetId (1);
 +                      ip.SetText (_("Thumbprint"));
 +                      ip.SetWidth (300);
 +
 +                      wxFont font = ip.GetFont ();
 +                      font.SetFamily (wxFONTFAMILY_TELETYPE);
 +                      ip.SetFont (font);
 +                      
 +                      _certificates->InsertColumn (1, ip);
 +              }
 +
 +              certificates_sizer->Add (_certificates, 1, wxEXPAND);
 +
 +              {
 +                      wxSizer* s = new wxBoxSizer (wxVERTICAL);
 +                      _add_certificate = new wxButton (_panel, wxID_ANY, _("Add..."));
 +                      s->Add (_add_certificate, 0, wxTOP | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
 +                      _remove_certificate = new wxButton (_panel, wxID_ANY, _("Remove"));
 +                      s->Add (_remove_certificate, 0, wxTOP | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
 +                      certificates_sizer->Add (s, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP);
 +              }
 +
 +              wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
 +              table->AddGrowableCol (1, 1);
 +              overall_sizer->Add (table, 1, wxALL | wxEXPAND, _border);
 +
 +              add_label_to_sizer (table, _panel, _("Private key for leaf certificate"), true);
 +              {
 +                      wxSizer* s = new wxBoxSizer (wxHORIZONTAL);
 +                      _signer_private_key = new wxStaticText (_panel, wxID_ANY, wxT (""));
 +                      wxFont font = _signer_private_key->GetFont ();
 +                      font.SetFamily (wxFONTFAMILY_TELETYPE);
 +                      _signer_private_key->SetFont (font);
 +                      s->Add (_signer_private_key, 1, wxLEFT | wxRIGHT | wxALIGN_CENTER_VERTICAL, DCPOMATIC_SIZER_X_GAP);
 +                      _load_signer_private_key = new wxButton (_panel, wxID_ANY, _("Load..."));
 +                      s->Add (_load_signer_private_key, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP);
 +                      table->Add (s, 0);
 +              }
 +
 +              add_label_to_sizer (table, _panel, _("Certificate for decrypting DCPs"), true);
 +              {
 +                      wxSizer* s = new wxBoxSizer (wxHORIZONTAL);
 +                      _decryption_certificate = new wxStaticText (_panel, wxID_ANY, wxT (""));
 +                      wxFont font = _decryption_certificate->GetFont ();
 +                      font.SetFamily (wxFONTFAMILY_TELETYPE);
 +                      _decryption_certificate->SetFont (font);
 +                      s->Add (_decryption_certificate, 1, wxLEFT | wxRIGHT | wxALIGN_CENTER_VERTICAL, DCPOMATIC_SIZER_X_GAP);
 +                      _load_decryption_certificate = new wxButton (_panel, wxID_ANY, _("Load..."));
 +                      s->Add (_load_decryption_certificate, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP);
 +                      table->Add (s, 0);
 +              }
 +
 +              add_label_to_sizer (table, _panel, _("Private key for decrypting DCPs"), true);
 +              {
 +                      wxSizer* s = new wxBoxSizer (wxHORIZONTAL);
 +                      _decryption_private_key = new wxStaticText (_panel, wxID_ANY, wxT (""));
 +                      wxFont font = _decryption_private_key->GetFont ();
 +                      font.SetFamily (wxFONTFAMILY_TELETYPE);
 +                      _decryption_private_key->SetFont (font);
 +                      s->Add (_decryption_private_key, 1, wxLEFT | wxRIGHT | wxALIGN_CENTER_VERTICAL, DCPOMATIC_SIZER_X_GAP);
 +                      _load_decryption_private_key = new wxButton (_panel, wxID_ANY, _("Load..."));
 +                      s->Add (_load_decryption_private_key, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP);
 +                      table->Add (s, 0);
 +              }
 +
 +              _export_decryption_certificate = new wxButton (_panel, wxID_ANY, _("Export DCP decryption certificate..."));
 +              table->Add (_export_decryption_certificate);
 +              table->AddSpacer (0);
 +              
 +              _add_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::add_certificate, this));
 +              _remove_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::remove_certificate, this));
 +              _certificates->Bind (wxEVT_COMMAND_LIST_ITEM_SELECTED, boost::bind (&KeysPage::update_sensitivity, this));
 +              _certificates->Bind (wxEVT_COMMAND_LIST_ITEM_DESELECTED, boost::bind (&KeysPage::update_sensitivity, this));
 +              _load_signer_private_key->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::load_signer_private_key, this));
 +              _load_decryption_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::load_decryption_certificate, this));
 +              _load_decryption_private_key->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::load_decryption_private_key, this));
 +              _export_decryption_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::export_decryption_certificate, this));
 +
 +              _signer.reset (new dcp::Signer (*Config::instance()->signer().get ()));
 +
 +              update_certificate_list ();
 +              update_signer_private_key ();
 +              update_decryption_certificate ();
 +              update_decryption_private_key ();
 +              update_sensitivity ();
 +
 +              return _panel;
 +      }
 +
 +private:
 +      void add_certificate ()
 +      {
 +              wxFileDialog* d = new wxFileDialog (_panel, _("Select Certificate File"));
 +              
 +              if (d->ShowModal() == wxID_OK) {
 +                      try {
 +                              dcp::Certificate c (dcp::file_to_string (wx_to_std (d->GetPath ())));
 +                              _signer->certificates().add (c);
 +                              Config::instance()->set_signer (_signer);
 +                              update_certificate_list ();
 +                      } catch (dcp::MiscError& e) {
 +                              error_dialog (_panel, wxString::Format (_("Could not read certificate file (%s)"), e.what ()));
 +                      }
 +              }
 +              
 +              d->Destroy ();
 +
 +              update_sensitivity ();
 +      }
 +
 +      void remove_certificate ()
 +      {
 +              int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
 +              if (i == -1) {
 +                      return;
 +              }
 +              
 +              _certificates->DeleteItem (i);
 +              _signer->certificates().remove (i);
 +              Config::instance()->set_signer (_signer);
 +
 +              update_sensitivity ();
 +      }
 +
 +      void update_certificate_list ()
 +      {
 +              _certificates->DeleteAllItems ();
 +              dcp::CertificateChain::List certs = _signer->certificates().root_to_leaf ();
 +              size_t n = 0;
 +              for (dcp::CertificateChain::List::const_iterator i = certs.begin(); i != certs.end(); ++i) {
 +                      wxListItem item;
 +                      item.SetId (n);
 +                      _certificates->InsertItem (item);
 +                      _certificates->SetItem (n, 1, std_to_wx (i->thumbprint ()));
 +
 +                      if (n == 0) {
 +                              _certificates->SetItem (n, 0, _("Root"));
 +                      } else if (n == (certs.size() - 1)) {
 +                              _certificates->SetItem (n, 0, _("Leaf"));
 +                      } else {
 +                              _certificates->SetItem (n, 0, _("Intermediate"));
 +                      }
 +
 +                      ++n;
 +              }
 +      }
 +
 +      void update_sensitivity ()
 +      {
 +              _remove_certificate->Enable (_certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED) != -1);
 +      }
 +
 +      void update_signer_private_key ()
 +      {
 +              _signer_private_key->SetLabel (std_to_wx (dcp::private_key_fingerprint (_signer->key ())));
 +      }       
 +
 +      void load_signer_private_key ()
 +      {
 +              wxFileDialog* d = new wxFileDialog (_panel, _("Select Key File"));
 +
 +              if (d->ShowModal() == wxID_OK) {
 +                      try {
 +                              boost::filesystem::path p (wx_to_std (d->GetPath ()));
 +                              if (boost::filesystem::file_size (p) > 1024) {
 +                                      error_dialog (_panel, wxString::Format (_("Could not read key file (%s)"), std_to_wx (p.string ())));
 +                                      return;
 +                              }
 +                              
 +                              _signer->set_key (dcp::file_to_string (p));
 +                              Config::instance()->set_signer (_signer);
 +                              update_signer_private_key ();
 +                      } catch (dcp::MiscError& e) {
 +                              error_dialog (_panel, wxString::Format (_("Could not read certificate file (%s)"), e.what ()));
 +                      }
 +              }
 +              
 +              d->Destroy ();
 +
 +              update_sensitivity ();
 +
 +      }
 +
 +      void load_decryption_certificate ()
 +      {
 +              wxFileDialog* d = new wxFileDialog (_panel, _("Select Certificate File"));
 +              
 +              if (d->ShowModal() == wxID_OK) {
 +                      try {
 +                              dcp::Certificate c (dcp::file_to_string (wx_to_std (d->GetPath ())));
 +                              Config::instance()->set_decryption_certificate (c);
 +                              update_decryption_certificate ();
 +                      } catch (dcp::MiscError& e) {
 +                              error_dialog (_panel, wxString::Format (_("Could not read certificate file (%s)"), e.what ()));
 +                      }
 +              }
 +              
 +              d->Destroy ();
 +      }
 +
 +      void update_decryption_certificate ()
 +      {
 +              _decryption_certificate->SetLabel (std_to_wx (Config::instance()->decryption_certificate().thumbprint ()));
 +      }
 +
 +      void load_decryption_private_key ()
 +      {
 +              wxFileDialog* d = new wxFileDialog (_panel, _("Select Key File"));
 +
 +              if (d->ShowModal() == wxID_OK) {
 +                      try {
 +                              boost::filesystem::path p (wx_to_std (d->GetPath ()));
 +                              Config::instance()->set_decryption_private_key (dcp::file_to_string (p));
 +                              update_decryption_private_key ();
 +                      } catch (dcp::MiscError& e) {
 +                              error_dialog (_panel, wxString::Format (_("Could not read key file (%s)"), e.what ()));
 +                      }
 +              }
 +              
 +              d->Destroy ();
 +      }
 +
 +      void update_decryption_private_key ()
 +      {
 +              _decryption_private_key->SetLabel (std_to_wx (dcp::private_key_fingerprint (Config::instance()->decryption_private_key())));
 +      }
 +
 +      void export_decryption_certificate ()
 +      {
 +              wxFileDialog* d = new wxFileDialog (
 +                      _panel, _("Select Certificate File"), wxEmptyString, wxEmptyString, wxT ("PEM files (*.pem)|*.pem"),
 +                      wxFD_SAVE | wxFD_OVERWRITE_PROMPT
 +                      );
 +              
 +              if (d->ShowModal () == wxID_OK) {
 +                      FILE* f = fopen_boost (wx_to_std (d->GetPath ()), "w");
 +                      if (!f) {
 +                              throw OpenFileError (wx_to_std (d->GetPath ()));
 +                      }
 +
 +                      string const s = Config::instance()->decryption_certificate().certificate (true);
 +                      fwrite (s.c_str(), 1, s.length(), f);
 +                      fclose (f);
 +              }
 +              d->Destroy ();
 +      }
 +
 +      wxPanel* _panel;
 +      wxListCtrl* _certificates;
 +      wxButton* _add_certificate;
 +      wxButton* _remove_certificate;
 +      wxStaticText* _signer_private_key;
 +      wxButton* _load_signer_private_key;
 +      wxStaticText* _decryption_certificate;
 +      wxButton* _load_decryption_certificate;
 +      wxStaticText* _decryption_private_key;
 +      wxButton* _load_decryption_private_key;
 +      wxButton* _export_decryption_certificate;
 +      shared_ptr<dcp::Signer> _signer;
 +};
 +
  class TMSPage : public wxPreferencesPage, public Page
  {
  public:
@@@ -1081,9 -772,6 +1081,9 @@@ private
        wxButton* _reset_kdm_email;
  };
  
 +/** @class AdvancedPage
 + *  @brief Advanced page of the preferences dialog.
 + */
  class AdvancedPage : public wxStockPreferencesPage, public Page
  {
  public:
@@@ -1208,7 -896,6 +1208,7 @@@ create_config_dialog (
        e->AddPage (new DefaultsPage (ps, border));
        e->AddPage (new EncodingServersPage (ps, border));
        e->AddPage (new ColourConversionsPage (ps, border));
 +      e->AddPage (new KeysPage (ps, border));
        e->AddPage (new TMSPage (ps, border));
        e->AddPage (new KDMEmailPage (ps, border));
        e->AddPage (new AdvancedPage (ps, border));