Tidy up interop API.
[libdcp.git] / src / dcp.cc
1 /*
2     Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
3
4     This program is free software; you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation; either version 2 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
18 */
19
20 /** @file  src/dcp.cc
21  *  @brief A class to create a DCP.
22  */
23
24 #include <sstream>
25 #include <fstream>
26 #include <iomanip>
27 #include <cassert>
28 #include <iostream>
29 #include <boost/filesystem.hpp>
30 #include <boost/lexical_cast.hpp>
31 #include <boost/algorithm/string.hpp>
32 #include <boost/lexical_cast.hpp>
33 #include <libxml++/libxml++.h>
34 #include <xmlsec/xmldsig.h>
35 #include <xmlsec/app.h>
36 #include "dcp.h"
37 #include "asset.h"
38 #include "sound_asset.h"
39 #include "picture_asset.h"
40 #include "subtitle_asset.h"
41 #include "util.h"
42 #include "metadata.h"
43 #include "exceptions.h"
44 #include "parse/pkl.h"
45 #include "parse/asset_map.h"
46 #include "reel.h"
47 #include "cpl.h"
48 #include "encryption.h"
49 #include "kdm.h"
50
51 using std::string;
52 using std::list;
53 using std::stringstream;
54 using std::ofstream;
55 using std::ostream;
56 using boost::shared_ptr;
57 using boost::lexical_cast;
58 using namespace libdcp;
59
60 DCP::DCP (string directory)
61         : _directory (directory)
62 {
63         boost::filesystem::create_directories (directory);
64 }
65
66 void
67 DCP::write_xml (bool interop, XMLMetadata const & metadata, shared_ptr<Encryption> crypt) const
68 {
69         for (list<shared_ptr<CPL> >::const_iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
70                 (*i)->write_xml (interop, metadata, crypt);
71         }
72
73         string pkl_uuid = make_uuid ();
74         string pkl_path = write_pkl (pkl_uuid, interop, metadata, crypt);
75         
76         write_volindex ();
77         write_assetmap (pkl_uuid, boost::filesystem::file_size (pkl_path), interop, metadata);
78 }
79
80 std::string
81 DCP::write_pkl (string pkl_uuid, bool interop, XMLMetadata const & metadata, shared_ptr<Encryption> crypt) const
82 {
83         assert (!_cpls.empty ());
84         
85         boost::filesystem::path p;
86         p /= _directory;
87         stringstream s;
88         s << pkl_uuid << "_pkl.xml";
89         p /= s.str();
90
91         xmlpp::Document doc;
92         xmlpp::Element* pkl;
93         if (interop) {
94                 pkl = doc.create_root_node("PackingList", "http://www.digicine.com/PROTO-ASDCP-PKL-20040311#");
95         } else {
96                 pkl = doc.create_root_node("PackingList", "http://www.smpte-ra.org/schemas/429-8/2007/PKL");
97         }
98         
99         if (crypt) {
100                 pkl->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig");
101         }
102
103         pkl->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
104         /* XXX: this is a bit of a hack */
105         pkl->add_child("AnnotationText")->add_child_text(_cpls.front()->name());
106         pkl->add_child("IssueDate")->add_child_text (metadata.issue_date);
107         pkl->add_child("Issuer")->add_child_text (metadata.issuer);
108         pkl->add_child("Creator")->add_child_text (metadata.creator);
109
110         xmlpp::Element* asset_list = pkl->add_child("AssetList");
111         list<shared_ptr<const Asset> > a = assets ();
112         for (list<shared_ptr<const Asset> >::const_iterator i = a.begin(); i != a.end(); ++i) {
113                 (*i)->write_to_pkl (asset_list);
114         }
115         
116         for (list<shared_ptr<CPL> >::const_iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
117                 (*i)->write_to_pkl (asset_list);
118         }
119
120         if (crypt) {
121                 sign (pkl, crypt->certificates, crypt->signer_key, interop);
122         }
123                 
124         doc.write_to_file_formatted (p.string (), "UTF-8");
125         return p.string ();
126 }
127
128 void
129 DCP::write_volindex () const
130 {
131         boost::filesystem::path p;
132         p /= _directory;
133         p /= "VOLINDEX.xml";
134
135         xmlpp::Document doc;
136         xmlpp::Element* root = doc.create_root_node ("VolumeIndex", "http://www.smpte-ra.org/schemas/429-9/2007/AM");
137         root->add_child("Index")->add_child_text ("1");
138         doc.write_to_file_formatted (p.string (), "UTF-8");
139 }
140
141 void
142 DCP::write_assetmap (string pkl_uuid, int pkl_length, bool interop, XMLMetadata const & metadata) const
143 {
144         boost::filesystem::path p;
145         p /= _directory;
146         p /= "ASSETMAP.xml";
147
148         xmlpp::Document doc;
149         xmlpp::Element* root;
150         if (interop) {
151                 root = doc.create_root_node ("AssetMap", "http://www.digicine.com/PROTO-ASDCP-AM-20040311#");
152         } else {
153                 root = doc.create_root_node ("AssetMap", "http://www.smpte-ra.org/schemas/429-9/2007/AM");
154         }
155
156         root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
157         root->add_child("Creator")->add_child_text (metadata.creator);
158         root->add_child("VolumeCount")->add_child_text ("1");
159         root->add_child("IssueDate")->add_child_text (metadata.issue_date);
160         root->add_child("Issuer")->add_child_text (metadata.issuer);
161         xmlpp::Node* asset_list = root->add_child ("AssetList");
162
163         xmlpp::Node* asset = asset_list->add_child ("Asset");
164         asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
165         asset->add_child("PackingList")->add_child_text ("true");
166         xmlpp::Node* chunk_list = asset->add_child ("ChunkList");
167         xmlpp::Node* chunk = chunk_list->add_child ("Chunk");
168         chunk->add_child("Path")->add_child_text (pkl_uuid + "_pkl.xml");
169         chunk->add_child("VolumeIndex")->add_child_text ("1");
170         chunk->add_child("Offset")->add_child_text ("0");
171         chunk->add_child("Length")->add_child_text (lexical_cast<string> (pkl_length));
172         
173         for (list<shared_ptr<CPL> >::const_iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
174                 (*i)->write_to_assetmap (asset_list);
175         }
176
177         list<shared_ptr<const Asset> > a = assets ();
178         for (list<shared_ptr<const Asset> >::const_iterator i = a.begin(); i != a.end(); ++i) {
179                 (*i)->write_to_assetmap (asset_list);
180         }
181
182         doc.write_to_file_formatted (p.string (), "UTF-8");
183 }
184
185
186 void
187 DCP::read (bool require_mxfs)
188 {
189         Files files;
190
191         shared_ptr<parse::AssetMap> asset_map;
192         try {
193                 boost::filesystem::path p = _directory;
194                 p /= "ASSETMAP";
195                 if (boost::filesystem::exists (p)) {
196                         asset_map.reset (new libdcp::parse::AssetMap (p.string ()));
197                 } else {
198                         p = _directory;
199                         p /= "ASSETMAP.xml";
200                         if (boost::filesystem::exists (p)) {
201                                 asset_map.reset (new libdcp::parse::AssetMap (p.string ()));
202                         } else {
203                                 boost::throw_exception (DCPReadError ("could not find AssetMap file"));
204                         }
205                 }
206                 
207         } catch (FileError& e) {
208                 boost::throw_exception (FileError ("could not load AssetMap file", files.asset_map));
209         }
210
211         for (list<shared_ptr<libdcp::parse::AssetMapAsset> >::const_iterator i = asset_map->assets.begin(); i != asset_map->assets.end(); ++i) {
212                 if ((*i)->chunks.size() != 1) {
213                         boost::throw_exception (XMLError ("unsupported asset chunk count"));
214                 }
215
216                 boost::filesystem::path t = _directory;
217                 t /= (*i)->chunks.front()->path;
218                 
219                 if (boost::algorithm::ends_with (t.string(), ".mxf") || boost::algorithm::ends_with (t.string(), ".ttf")) {
220                         continue;
221                 }
222
223                 xmlpp::DomParser* p = new xmlpp::DomParser;
224                 try {
225                         p->parse_file (t.string());
226                 } catch (std::exception& e) {
227                         delete p;
228                         continue;
229                 }
230
231                 string const root = p->get_document()->get_root_node()->get_name ();
232                 delete p;
233
234                 if (root == "CompositionPlaylist") {
235                         files.cpls.push_back (t.string());
236                 } else if (root == "PackingList") {
237                         if (files.pkl.empty ()) {
238                                 files.pkl = t.string();
239                         } else {
240                                 boost::throw_exception (DCPReadError ("duplicate PKLs found"));
241                         }
242                 }
243         }
244         
245         if (files.cpls.empty ()) {
246                 boost::throw_exception (FileError ("no CPL files found", ""));
247         }
248
249         if (files.pkl.empty ()) {
250                 boost::throw_exception (FileError ("no PKL file found", ""));
251         }
252
253         shared_ptr<parse::PKL> pkl;
254         try {
255                 pkl.reset (new parse::PKL (files.pkl));
256         } catch (FileError& e) {
257                 boost::throw_exception (FileError ("could not load PKL file", files.pkl));
258         }
259
260         /* Cross-check */
261         /* XXX */
262
263         for (list<string>::iterator i = files.cpls.begin(); i != files.cpls.end(); ++i) {
264                 _cpls.push_back (shared_ptr<CPL> (new CPL (_directory, *i, asset_map, require_mxfs)));
265         }
266 }
267
268 bool
269 DCP::equals (DCP const & other, EqualityOptions opt, boost::function<void (NoteType, string)> note) const
270 {
271         if (_cpls.size() != other._cpls.size()) {
272                 note (ERROR, "CPL counts differ");
273                 return false;
274         }
275
276         list<shared_ptr<CPL> >::const_iterator a = _cpls.begin ();
277         list<shared_ptr<CPL> >::const_iterator b = other._cpls.begin ();
278
279         while (a != _cpls.end ()) {
280                 if (!(*a)->equals (*b->get(), opt, note)) {
281                         return false;
282                 }
283                 ++a;
284                 ++b;
285         }
286
287         return true;
288 }
289
290 void
291 DCP::add_cpl (shared_ptr<CPL> cpl)
292 {
293         _cpls.push_back (cpl);
294 }
295
296 class AssetComparator
297 {
298 public:
299         bool operator() (shared_ptr<const Asset> a, shared_ptr<const Asset> b) {
300                 return a->uuid() < b->uuid();
301         }
302 };
303
304 list<shared_ptr<const Asset> >
305 DCP::assets () const
306 {
307         list<shared_ptr<const Asset> > a;
308         for (list<shared_ptr<CPL> >::const_iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
309                 list<shared_ptr<const Asset> > t = (*i)->assets ();
310                 a.merge (t);
311         }
312
313         a.sort (AssetComparator ());
314         a.unique ();
315         return a;
316 }
317
318 bool
319 DCP::encrypted () const
320 {
321         for (list<shared_ptr<CPL> >::const_iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
322                 if ((*i)->encrypted ()) {
323                         return true;
324                 }
325         }
326
327         return false;
328 }
329
330 void
331 DCP::add_kdm (KDM const & kdm)
332 {
333         list<KDMCipher> ciphers = kdm.ciphers ();
334         
335         for (list<shared_ptr<CPL> >::iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
336                 for (list<KDMCipher>::iterator j = ciphers.begin(); j != ciphers.end(); ++j) {
337                         if (j->cpl_id() == (*i)->id()) {
338                                 (*i)->add_kdm (kdm);
339                         }                               
340                 }
341         }
342 }