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