38e43eb1425229b47e2fb0730b457b3e35deb7dd
[libdcp.git] / tools / dcpinfo.cc
1 /*
2     Copyright (C) 2012-2020 Carl Hetherington <cth@carlh.net>
3
4     This file is part of libdcp.
5
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.
10
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.
15
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/>.
18
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
23     including the two.
24
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.
32 */
33
34 #include "dcp.h"
35 #include "exceptions.h"
36 #include "reel.h"
37 #include "sound_asset.h"
38 #include "picture_asset.h"
39 #include "subtitle_asset.h"
40 #include "reel_picture_asset.h"
41 #include "reel_sound_asset.h"
42 #include "reel_subtitle_asset.h"
43 #include "subtitle_string.h"
44 #include "subtitle_image.h"
45 #include "interop_subtitle_asset.h"
46 #include "smpte_subtitle_asset.h"
47 #include "mono_picture_asset.h"
48 #include "encrypted_kdm.h"
49 #include "decrypted_kdm.h"
50 #include "cpl.h"
51 #include "common.h"
52 #include "compose.hpp"
53 #include <getopt.h>
54 #include <boost/filesystem.hpp>
55 #include <boost/foreach.hpp>
56 #include <boost/algorithm/string.hpp>
57 #include <iostream>
58 #include <cstdlib>
59 #include <sstream>
60 #include <inttypes.h>
61
62 using std::string;
63 using std::cerr;
64 using std::cout;
65 using std::list;
66 using std::pair;
67 using std::min;
68 using std::max;
69 using std::exception;
70 using std::vector;
71 using std::stringstream;
72 using boost::shared_ptr;
73 using boost::dynamic_pointer_cast;
74 using boost::optional;
75 using namespace dcp;
76
77 static void
78 help (string n)
79 {
80         cerr << "Syntax: " << n << " [options] [<DCP>] [<CPL>]\n"
81              << "  -s, --subtitles              list all subtitles\n"
82              << "  -p, --picture                analyse picture\n"
83              << "  -d, --decompress             decompress picture when analysing (this is slow)\n"
84              << "  -o, --only                   only output certain pieces of information; see below.\n"
85              << "      --kdm                    KDM to decrypt DCP\n"
86              << "      --private-key            private key for the certificate that the KDM is targeted at\n"
87              << "      --ignore-missing-assets  ignore missing asset files\n";
88
89         cerr << "--only takes a comma-separated list of strings, one or more of:\n"
90                 "    dcp-path     DCP path\n"
91                 "    cpl-name-id  CPL name and ID\n"
92                 "    picture      picture information\n"
93                 "    sound        sound information\n"
94                 "    subtitle     picture information\n"
95                 "    total-time   total DCP time\n";
96 }
97
98 static double
99 mbits_per_second (int size, Fraction frame_rate)
100 {
101         return size * 8 * frame_rate.as_float() / 1e6;
102 }
103
104 #define OUTPUT_DCP_PATH(...)    maybe_output(only, "dcp-path", String::compose(__VA_ARGS__));
105 #define OUTPUT_CPL_NAME_ID(...) maybe_output(only, "cpl-name-id", String::compose(__VA_ARGS__));
106 #define OUTPUT_PICTURE(...)     maybe_output(only, "picture", String::compose(__VA_ARGS__));
107 #define OUTPUT_PICTURE_NC(x)    maybe_output(only, "picture", (x));
108 #define SHOULD_PICTURE          should_output(only, "picture")
109 #define OUTPUT_SOUND(...)       maybe_output(only, "sound", String::compose(__VA_ARGS__));
110 #define OUTPUT_SOUND_NC(x)      maybe_output(only, "sound", (x));
111 #define OUTPUT_SUBTITLE(...)    maybe_output(only, "subtitle", String::compose(__VA_ARGS__));
112 #define OUTPUT_SUBTITLE_NC(x)   maybe_output(only, "subtitle", (x));
113 #define OUTPUT_TOTAL_TIME(...)  maybe_output(only, "total-time", String::compose(__VA_ARGS__));
114
115 static bool
116 should_output(vector<string> const& only, string t)
117 {
118         return only.empty() || find(only.begin(), only.end(), t) != only.end();
119 }
120
121 static void
122 maybe_output(vector<string> const& only, string t, string s)
123 {
124         if (should_output(only, t)) {
125                 cout << s;
126         }
127 }
128
129 static
130 dcp::Time
131 main_picture (vector<string> const& only, shared_ptr<Reel> reel, bool analyse, bool decompress)
132 {
133         shared_ptr<dcp::ReelPictureAsset> mp = reel->main_picture ();
134         if (!mp) {
135                 return dcp::Time();
136         }
137
138         OUTPUT_PICTURE("      Picture ID:  %1", mp->id());
139         if (mp->entry_point()) {
140                 OUTPUT_PICTURE(" entry %1", *mp->entry_point());
141         }
142         if (mp->duration()) {
143                 OUTPUT_PICTURE(
144                         " duration %1 (%2) intrinsic %3",
145                         *mp->duration(),
146                         dcp::Time(*mp->duration(), mp->frame_rate().as_float(), mp->frame_rate().as_float()).as_string(dcp::SMPTE),
147                         mp->intrinsic_duration()
148                         );
149         } else {
150                 OUTPUT_PICTURE(" intrinsic duration %1", mp->intrinsic_duration());
151         }
152
153         if (mp->asset_ref().resolved()) {
154                 if (mp->asset()) {
155                         OUTPUT_PICTURE("\n      Picture:     %1x%2\n", mp->asset()->size().width, mp->asset()->size().height);
156                 }
157
158                 shared_ptr<MonoPictureAsset> ma = dynamic_pointer_cast<MonoPictureAsset>(mp->asset());
159                 if (analyse && ma) {
160                         shared_ptr<MonoPictureAssetReader> reader = ma->start_read ();
161                         pair<int, int> j2k_size_range (INT_MAX, 0);
162                         for (int64_t i = 0; i < ma->intrinsic_duration(); ++i) {
163                                 shared_ptr<const MonoPictureFrame> frame = reader->get_frame (i);
164                                 if (SHOULD_PICTURE) {
165                                         printf("Frame %" PRId64 " J2K size %7d", i, frame->j2k_size());
166                                 }
167                                 j2k_size_range.first = min(j2k_size_range.first, frame->j2k_size());
168                                 j2k_size_range.second = max(j2k_size_range.second, frame->j2k_size());
169
170                                 if (decompress) {
171                                         try {
172                                                 frame->xyz_image();
173                                                 if (SHOULD_PICTURE) {
174                                                         printf(" decrypted OK");
175                                                 }
176                                         } catch (exception& e) {
177                                                 if (SHOULD_PICTURE) {
178                                                         printf(" decryption FAILED");
179                                                 }
180                                         }
181                                 }
182
183                                 if (SHOULD_PICTURE) {
184                                         printf("\n");
185                                 }
186
187                         }
188                         if (SHOULD_PICTURE) {
189                                 printf(
190                                                 "J2K size ranges from %d (%.1f Mbit/s) to %d (%.1f Mbit/s)\n",
191                                                 j2k_size_range.first, mbits_per_second(j2k_size_range.first, ma->frame_rate()),
192                                                 j2k_size_range.second, mbits_per_second(j2k_size_range.second, ma->frame_rate())
193                                       );
194                         }
195                 }
196         } else {
197                 OUTPUT_PICTURE_NC(" - not present in this DCP.\n");
198         }
199
200         return dcp::Time (
201                         mp->duration().get_value_or(mp->intrinsic_duration()),
202                         mp->frame_rate().as_float(),
203                         mp->frame_rate().as_float()
204                         );
205 }
206
207 static
208 void
209 main_sound (vector<string> const& only, shared_ptr<Reel> reel)
210 {
211         shared_ptr<dcp::ReelSoundAsset> ms = reel->main_sound ();
212         if (!ms) {
213                 return;
214         }
215
216         OUTPUT_SOUND("      Sound ID:    %1", ms->id());
217         if (ms->entry_point()) {
218                 OUTPUT_SOUND(" entry %1", *ms->entry_point());
219         }
220         if (ms->duration()) {
221                 OUTPUT_SOUND(" duration %1 intrinsic %2", *ms->duration(), ms->intrinsic_duration());
222         } else {
223                 OUTPUT_SOUND(" intrinsic duration %1", ms->intrinsic_duration());
224         }
225
226         if (ms->asset_ref().resolved()) {
227                 if (ms->asset()) {
228                         OUTPUT_SOUND(
229                                 "\n      Sound:       %1 channels at %2Hz\n",
230                                 ms->asset()->channels(),
231                                 ms->asset()->sampling_rate()
232                                 );
233                 }
234         } else {
235                 OUTPUT_SOUND_NC(" - not present in this DCP.\n");
236         }
237 }
238
239 static
240 void
241 main_subtitle (vector<string> const& only, shared_ptr<Reel> reel, bool list_subtitles)
242 {
243         shared_ptr<dcp::ReelSubtitleAsset> ms = reel->main_subtitle ();
244         if (!ms) {
245                 return;
246         }
247
248         OUTPUT_SUBTITLE("      Subtitle ID: %1", ms->id());
249
250         if (ms->asset_ref().resolved()) {
251                 list<shared_ptr<Subtitle> > subs = ms->asset()->subtitles ();
252                 OUTPUT_SUBTITLE("\n      Subtitle:    %1 subtitles", subs.size());
253                 shared_ptr<InteropSubtitleAsset> iop = dynamic_pointer_cast<InteropSubtitleAsset> (ms->asset());
254                 if (iop) {
255                         OUTPUT_SUBTITLE(" in %1\n", iop->language());
256                 }
257                 shared_ptr<SMPTESubtitleAsset> smpte = dynamic_pointer_cast<SMPTESubtitleAsset> (ms->asset());
258                 if (smpte && smpte->language ()) {
259                         OUTPUT_SUBTITLE(" in %1\n", smpte->language().get());
260                 }
261                 if (list_subtitles) {
262                         BOOST_FOREACH (shared_ptr<Subtitle> k, subs) {
263                                 shared_ptr<SubtitleString> ks = dynamic_pointer_cast<SubtitleString> (k);
264                                 if (ks) {
265                                         stringstream s;
266                                         s << *ks;
267                                         OUTPUT_SUBTITLE("%1\n", s.str());
268                                 }
269                                 shared_ptr<SubtitleImage> is = dynamic_pointer_cast<SubtitleImage> (k);
270                                 if (is) {
271                                         stringstream s;
272                                         s << *is;
273                                         OUTPUT_SUBTITLE("%1\n", s.str());
274                                 }
275                         }
276                 }
277         } else {
278                 OUTPUT_SUBTITLE_NC(" - not present in this DCP.\n");
279         }
280 }
281
282
283 int
284 main (int argc, char* argv[])
285 {
286         bool subtitles = false;
287         bool picture = false;
288         bool decompress = false;
289         bool ignore_missing_assets = false;
290         optional<boost::filesystem::path> kdm;
291         optional<boost::filesystem::path> private_key;
292         optional<string> only_string;
293
294         int option_index = 0;
295         while (true) {
296                 static struct option long_options[] = {
297                         { "version", no_argument, 0, 'v' },
298                         { "help", no_argument, 0, 'h' },
299                         { "subtitles", no_argument, 0, 's' },
300                         { "picture", no_argument, 0, 'p' },
301                         { "decompress", no_argument, 0, 'd' },
302                         { "only", required_argument, 0, 'o' },
303                         { "ignore-missing-assets", no_argument, 0, 'A' },
304                         { "kdm", required_argument, 0, 'B' },
305                         { "private-key", required_argument, 0, 'C' },
306                         { 0, 0, 0, 0 }
307                 };
308
309                 int c = getopt_long (argc, argv, "vhspdo:AB:C:", long_options, &option_index);
310
311                 if (c == -1) {
312                         break;
313                 }
314
315                 switch (c) {
316                 case 'v':
317                         cout << "libdcp version " << LIBDCP_VERSION << "\n";
318                         exit (EXIT_SUCCESS);
319                 case 'h':
320                         help (argv[0]);
321                         exit (EXIT_SUCCESS);
322                 case 's':
323                         subtitles = true;
324                         break;
325                 case 'p':
326                         picture = true;
327                         break;
328                 case 'd':
329                         decompress = true;
330                         break;
331                 case 'o':
332                         only_string = optarg;
333                         break;
334                 case 'A':
335                         ignore_missing_assets = true;
336                         break;
337                 case 'B':
338                         kdm = optarg;
339                         break;
340                 case 'C':
341                         private_key = optarg;
342                         break;
343                 }
344         }
345
346         if (argc <= optind || argc > (optind + 1)) {
347                 help (argv[0]);
348                 exit (EXIT_FAILURE);
349         }
350
351         if (!boost::filesystem::exists (argv[optind])) {
352                 cerr << argv[0] << ": DCP or CPL " << argv[optind] << " not found.\n";
353                 exit (EXIT_FAILURE);
354         }
355
356         vector<string> only;
357         if (only_string) {
358                 only = boost::split(only, *only_string, boost::is_any_of(","));
359         }
360
361         list<shared_ptr<CPL> > cpls;
362         if (boost::filesystem::is_directory(argv[optind])) {
363                 DCP* dcp = 0;
364                 list<dcp::VerificationNote> notes;
365                 try {
366                         dcp = new DCP (argv[optind]);
367                         dcp->read (&notes);
368                         if (kdm && private_key) {
369                                 dcp->add(DecryptedKDM(EncryptedKDM(file_to_string(*kdm)), file_to_string(*private_key)));
370                         }
371                 } catch (FileError& e) {
372                         cerr << "Could not read DCP " << argv[optind] << "; " << e.what() << "\n";
373                         exit (EXIT_FAILURE);
374                 } catch (ReadError& e) {
375                         cerr << "Could not read DCP " << argv[optind] << "; " << e.what() << "\n";
376                         exit (EXIT_FAILURE);
377                 } catch (KDMDecryptionError& e) {
378                         cerr << e.what() << "\n";
379                         exit (EXIT_FAILURE);
380                 }
381
382                 OUTPUT_DCP_PATH("DCP: %1\n", boost::filesystem::path(argv[optind]).string());
383
384                 dcp::filter_notes (notes, ignore_missing_assets);
385                 BOOST_FOREACH (dcp::VerificationNote i, notes) {
386                         cerr << "Error: " << note_to_string(i) << "\n";
387                 }
388
389                 cpls = dcp->cpls ();
390         } else {
391                 cpls.push_back (shared_ptr<CPL>(new CPL(boost::filesystem::path(argv[optind]))));
392                 ignore_missing_assets = true;
393         }
394
395         dcp::Time total_time;
396
397         BOOST_FOREACH (shared_ptr<CPL> i, cpls) {
398                 OUTPUT_CPL_NAME_ID("  CPL: %1 %2\n", i->annotation_text(), i->id());
399
400                 int R = 1;
401                 BOOST_FOREACH (shared_ptr<Reel> j, i->reels()) {
402                         if (should_output(only, "picture") || should_output(only, "sound") || should_output(only, "subtitle")) {
403                                 cout << "    Reel " << R << "\n";
404                         }
405
406                         try {
407                                 total_time += main_picture(only, j, picture, decompress);
408                         } catch (UnresolvedRefError& e) {
409                                 if (!ignore_missing_assets) {
410                                         cerr << e.what() << " (for main picture)\n";
411                                 }
412                         }
413
414                         try {
415                                 main_sound(only, j);
416                         } catch (UnresolvedRefError& e) {
417                                 if (!ignore_missing_assets) {
418                                         cerr << e.what() << " (for main sound)\n";
419                                 }
420                         }
421
422                         try {
423                                 main_subtitle (only, j, subtitles);
424                         } catch (UnresolvedRefError& e) {
425                                 if (!ignore_missing_assets) {
426                                         cerr << e.what() << " (for main subtitle)\n";
427                                 }
428                         }
429
430                         ++R;
431                 }
432         }
433
434         OUTPUT_TOTAL_TIME("Total: %1\n", total_time.as_string(dcp::SMPTE));
435
436         return 0;
437 }